1use std::any::{Any, TypeId};
36use std::collections::{HashMap, HashSet, VecDeque};
37use std::fmt;
38use std::hash::{Hash, Hasher};
39use std::marker::PhantomData;
40use std::path::{Path, PathBuf};
41use std::sync::{Arc, RwLock, Weak};
42use std::time::{Duration, Instant, SystemTime};
43
44pub trait Asset: Send + Sync + 'static {}
60
61pub trait AssetLoader<A: Asset>: Send + Sync + 'static {
69 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<A, String>;
76
77 fn extensions(&self) -> &[&str];
81}
82
83pub trait AssetProcessor<A: Asset>: Send + Sync + 'static {
88 fn process(&self, asset: &mut A, path: &AssetPath) -> Result<(), String>;
90
91 fn name(&self) -> &str;
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
115pub struct AssetPath {
116 path: PathBuf,
117 label: Option<String>,
118}
119
120impl AssetPath {
121 pub fn new<P: AsRef<Path>>(path: P) -> Self {
123 Self {
124 path: path.as_ref().to_path_buf(),
125 label: None,
126 }
127 }
128
129 pub fn with_label<P: AsRef<Path>>(path: P, label: impl Into<String>) -> Self {
131 Self {
132 path: path.as_ref().to_path_buf(),
133 label: Some(label.into()),
134 }
135 }
136
137 pub fn parse(s: &str) -> Self {
141 match s.find('#') {
142 Some(idx) => Self {
143 path: PathBuf::from(&s[..idx]),
144 label: Some(s[idx + 1..].to_owned()),
145 },
146 None => Self {
147 path: PathBuf::from(s),
148 label: None,
149 },
150 }
151 }
152
153 pub fn path(&self) -> &Path {
155 &self.path
156 }
157
158 pub fn label(&self) -> Option<&str> {
160 self.label.as_deref()
161 }
162
163 pub fn extension(&self) -> Option<String> {
165 self.path
166 .extension()
167 .and_then(|e| e.to_str())
168 .map(|e| e.to_lowercase())
169 }
170
171 pub fn to_string_repr(&self) -> String {
173 match &self.label {
174 Some(l) => format!("{}#{}", self.path.display(), l),
175 None => self.path.display().to_string(),
176 }
177 }
178}
179
180impl fmt::Display for AssetPath {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "{}", self.to_string_repr())
183 }
184}
185
186impl From<&str> for AssetPath {
187 fn from(s: &str) -> Self {
188 Self::parse(s)
189 }
190}
191
192impl From<String> for AssetPath {
193 fn from(s: String) -> Self {
194 Self::parse(&s)
195 }
196}
197
198#[derive(Debug)]
207pub struct AssetId<T: Asset> {
208 id: u64,
209 _marker: PhantomData<fn() -> T>,
210}
211
212impl<T: Asset> AssetId<T> {
213 fn new(id: u64) -> Self {
214 Self { id, _marker: PhantomData }
215 }
216
217 pub fn raw(&self) -> u64 {
219 self.id
220 }
221}
222
223impl<T: Asset> Clone for AssetId<T> {
224 fn clone(&self) -> Self {
225 Self::new(self.id)
226 }
227}
228
229impl<T: Asset> Copy for AssetId<T> {}
230
231impl<T: Asset> PartialEq for AssetId<T> {
232 fn eq(&self, other: &Self) -> bool {
233 self.id == other.id
234 }
235}
236
237impl<T: Asset> Eq for AssetId<T> {}
238
239impl<T: Asset> Hash for AssetId<T> {
240 fn hash<H: Hasher>(&self, state: &mut H) {
241 self.id.hash(state);
242 }
243}
244
245impl<T: Asset> fmt::Display for AssetId<T> {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 write!(f, "AssetId({})", self.id)
248 }
249}
250
251#[derive(Debug)]
259pub struct AssetHandle<T: Asset> {
260 id: AssetId<T>,
261 inner: Arc<RwLock<Option<T>>>,
262}
263
264impl<T: Asset> AssetHandle<T> {
265 fn new(id: AssetId<T>, inner: Arc<RwLock<Option<T>>>) -> Self {
266 Self { id, inner }
267 }
268
269 pub fn id(&self) -> AssetId<T> {
271 self.id
272 }
273
274 pub fn downgrade(&self) -> WeakHandle<T> {
276 WeakHandle {
277 id: self.id,
278 inner: Arc::downgrade(&self.inner),
279 }
280 }
281
282 pub fn is_loaded(&self) -> bool {
284 self.inner
285 .read()
286 .map(|g| g.is_some())
287 .unwrap_or(false)
288 }
289}
290
291impl<T: Asset> Clone for AssetHandle<T> {
292 fn clone(&self) -> Self {
293 Self {
294 id: self.id,
295 inner: Arc::clone(&self.inner),
296 }
297 }
298}
299
300impl<T: Asset> PartialEq for AssetHandle<T> {
301 fn eq(&self, other: &Self) -> bool {
302 self.id == other.id
303 }
304}
305
306impl<T: Asset> Eq for AssetHandle<T> {}
307
308#[derive(Debug, Clone)]
313pub struct WeakHandle<T: Asset> {
314 id: AssetId<T>,
315 inner: Weak<RwLock<Option<T>>>,
316}
317
318impl<T: Asset> WeakHandle<T> {
319 pub fn upgrade(&self) -> Option<AssetHandle<T>> {
323 self.inner.upgrade().map(|arc| AssetHandle::new(self.id, arc))
324 }
325
326 pub fn id(&self) -> AssetId<T> {
328 self.id
329 }
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
338pub enum LoadState {
339 NotLoaded,
341 Loading,
343 Loaded,
345 Failed(String),
347}
348
349impl fmt::Display for LoadState {
350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351 match self {
352 LoadState::NotLoaded => write!(f, "NotLoaded"),
353 LoadState::Loading => write!(f, "Loading"),
354 LoadState::Loaded => write!(f, "Loaded"),
355 LoadState::Failed(msg) => write!(f, "Failed: {msg}"),
356 }
357 }
358}
359
360#[derive(Debug, Default, Clone)]
370pub struct AssetDependency {
371 pub depends_on: HashSet<u64>,
373 pub depended_by: HashSet<u64>,
375}
376
377impl AssetDependency {
378 pub fn new() -> Self {
380 Self::default()
381 }
382
383 pub fn add(&mut self, owner: u64, dependency: u64) {
385 self.depends_on.insert(dependency);
386 let _ = owner;
387 }
388}
389
390struct ErasedSlot {
396 type_id: TypeId,
398 value: Arc<dyn Any + Send + Sync>,
400 state: LoadState,
402 path: AssetPath,
404 file_mtime: Option<SystemTime>,
406 dependency: AssetDependency,
408 access_count: u64,
410 last_access: Instant,
412}
413
414impl ErasedSlot {
415 fn new(type_id: TypeId, path: AssetPath) -> Self {
416 Self {
417 type_id,
418 value: Arc::new(()),
419 state: LoadState::NotLoaded,
420 path,
421 file_mtime: None,
422 dependency: AssetDependency::new(),
423 access_count: 0,
424 last_access: Instant::now(),
425 }
426 }
427}
428
429pub struct AssetRegistry {
440 slots: HashMap<u64, ErasedSlot>,
441 path_to_id: HashMap<AssetPath, u64>,
442 next_id: u64,
443}
444
445impl AssetRegistry {
446 pub fn new() -> Self {
448 Self {
449 slots: HashMap::new(),
450 path_to_id: HashMap::new(),
451 next_id: 1,
452 }
453 }
454
455 pub fn alloc(&mut self, type_id: TypeId, path: AssetPath) -> u64 {
457 let id = self.next_id;
458 self.next_id += 1;
459 self.path_to_id.insert(path.clone(), id);
460 self.slots.insert(id, ErasedSlot::new(type_id, path));
461 id
462 }
463
464 pub fn id_for_path(&self, path: &AssetPath) -> Option<u64> {
466 self.path_to_id.get(path).copied()
467 }
468
469 pub fn load_state(&self, id: u64) -> LoadState {
471 self.slots
472 .get(&id)
473 .map(|s| s.state.clone())
474 .unwrap_or(LoadState::NotLoaded)
475 }
476
477 pub fn mark_loading(&mut self, id: u64) {
479 if let Some(slot) = self.slots.get_mut(&id) {
480 slot.state = LoadState::Loading;
481 }
482 }
483
484 pub fn store<T: Asset>(&mut self, id: u64, value: T, mtime: Option<SystemTime>) {
486 if let Some(slot) = self.slots.get_mut(&id) {
487 slot.value = Arc::new(value);
488 slot.state = LoadState::Loaded;
489 slot.file_mtime = mtime;
490 slot.last_access = Instant::now();
491 }
492 }
493
494 pub fn mark_failed(&mut self, id: u64, message: String) {
496 if let Some(slot) = self.slots.get_mut(&id) {
497 slot.state = LoadState::Failed(message);
498 }
499 }
500
501 pub fn get<T: Asset>(&mut self, id: u64) -> Option<Arc<T>> {
503 let slot = self.slots.get_mut(&id)?;
504 if slot.state != LoadState::Loaded {
505 return None;
506 }
507 slot.access_count += 1;
508 slot.last_access = Instant::now();
509 Arc::clone(&slot.value).downcast::<T>().ok()
510 }
511
512 pub fn path_for_id(&self, id: u64) -> Option<&AssetPath> {
514 self.slots.get(&id).map(|s| &s.path)
515 }
516
517 pub fn type_id_for(&self, id: u64) -> Option<TypeId> {
519 self.slots.get(&id).map(|s| s.type_id)
520 }
521
522 pub fn all_ids(&self) -> impl Iterator<Item = u64> + '_ {
524 self.slots.keys().copied()
525 }
526
527 pub fn len(&self) -> usize {
529 self.slots.len()
530 }
531
532 pub fn is_empty(&self) -> bool {
534 self.slots.is_empty()
535 }
536
537 pub fn evict(&mut self, id: u64) -> bool {
539 if let Some(slot) = self.slots.remove(&id) {
540 self.path_to_id.remove(&slot.path);
541 true
542 } else {
543 false
544 }
545 }
546}
547
548impl Default for AssetRegistry {
549 fn default() -> Self {
550 Self::new()
551 }
552}
553
554impl fmt::Debug for AssetRegistry {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 f.debug_struct("AssetRegistry")
557 .field("slot_count", &self.slots.len())
558 .field("next_id", &self.next_id)
559 .finish()
560 }
561}
562
563pub struct AssetCache {
576 capacity: usize,
578 lru_queue: VecDeque<u64>,
580 lru_set: HashSet<u64>,
582}
583
584impl AssetCache {
585 pub fn new(capacity: usize) -> Self {
589 Self {
590 capacity,
591 lru_queue: VecDeque::new(),
592 lru_set: HashSet::new(),
593 }
594 }
595
596 pub fn touch(&mut self, id: u64) -> Option<u64> {
601 if self.lru_set.contains(&id) {
602 self.lru_queue.retain(|&x| x != id);
603 } else {
604 self.lru_set.insert(id);
605 }
606 self.lru_queue.push_back(id);
607
608 if self.capacity > 0 && self.lru_queue.len() > self.capacity {
609 let victim = self.lru_queue.pop_front().unwrap();
610 self.lru_set.remove(&victim);
611 Some(victim)
612 } else {
613 None
614 }
615 }
616
617 pub fn remove(&mut self, id: u64) {
619 self.lru_queue.retain(|&x| x != id);
620 self.lru_set.remove(&id);
621 }
622
623 pub fn len(&self) -> usize {
625 self.lru_queue.len()
626 }
627
628 pub fn is_empty(&self) -> bool {
630 self.lru_queue.is_empty()
631 }
632
633 pub fn capacity(&self) -> usize {
635 self.capacity
636 }
637
638 pub fn set_capacity(&mut self, new_cap: usize) -> Vec<u64> {
641 self.capacity = new_cap;
642 let mut evicted = Vec::new();
643 while new_cap > 0 && self.lru_queue.len() > new_cap {
644 if let Some(victim) = self.lru_queue.pop_front() {
645 self.lru_set.remove(&victim);
646 evicted.push(victim);
647 }
648 }
649 evicted
650 }
651}
652
653impl fmt::Debug for AssetCache {
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 f.debug_struct("AssetCache")
656 .field("capacity", &self.capacity)
657 .field("len", &self.lru_queue.len())
658 .finish()
659 }
660}
661
662#[derive(Debug, Clone)]
668struct WatchedFile {
669 path: PathBuf,
670 last_mtime: Option<SystemTime>,
671 asset_ids: Vec<u64>,
672}
673
674pub struct HotReload {
685 watched: HashMap<PathBuf, WatchedFile>,
686 poll_interval: Duration,
687 last_poll: Instant,
688 enabled: bool,
689}
690
691impl HotReload {
692 pub fn new(poll_interval: Duration, enabled: bool) -> Self {
697 Self {
698 watched: HashMap::new(),
699 poll_interval,
700 last_poll: Instant::now(),
701 enabled,
702 }
703 }
704
705 pub fn watch(&mut self, path: PathBuf, asset_id: u64, current_mtime: Option<SystemTime>) {
707 let entry = self.watched.entry(path.clone()).or_insert(WatchedFile {
708 path,
709 last_mtime: current_mtime,
710 asset_ids: Vec::new(),
711 });
712 if !entry.asset_ids.contains(&asset_id) {
713 entry.asset_ids.push(asset_id);
714 }
715 if current_mtime.is_some() {
716 entry.last_mtime = current_mtime;
717 }
718 }
719
720 pub fn unwatch(&mut self, asset_id: u64) {
722 self.watched.retain(|_, wf| {
723 wf.asset_ids.retain(|&id| id != asset_id);
724 !wf.asset_ids.is_empty()
725 });
726 }
727
728 pub fn poll(&mut self) -> Vec<(PathBuf, Vec<u64>)> {
733 if !self.enabled {
734 return Vec::new();
735 }
736 if self.last_poll.elapsed() < self.poll_interval {
737 return Vec::new();
738 }
739 self.last_poll = Instant::now();
740
741 let mut changed = Vec::new();
742 for wf in self.watched.values_mut() {
743 let current_mtime = std::fs::metadata(&wf.path)
744 .and_then(|m| m.modified())
745 .ok();
746 if current_mtime != wf.last_mtime {
747 wf.last_mtime = current_mtime;
748 changed.push((wf.path.clone(), wf.asset_ids.clone()));
749 }
750 }
751 changed
752 }
753
754 pub fn force_next_poll(&mut self) {
756 self.last_poll = Instant::now()
757 .checked_sub(self.poll_interval + Duration::from_millis(1))
758 .unwrap_or(Instant::now());
759 }
760
761 pub fn set_enabled(&mut self, enabled: bool) {
763 self.enabled = enabled;
764 }
765
766 pub fn watched_count(&self) -> usize {
768 self.watched.len()
769 }
770}
771
772impl fmt::Debug for HotReload {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 f.debug_struct("HotReload")
775 .field("enabled", &self.enabled)
776 .field("watched_files", &self.watched.len())
777 .field("poll_interval_ms", &self.poll_interval.as_millis())
778 .finish()
779 }
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
790pub enum StreamPriority {
791 Low = 0,
793 Normal = 1,
795 High = 2,
797 Critical = 3,
799}
800
801impl Default for StreamPriority {
802 fn default() -> Self {
803 StreamPriority::Normal
804 }
805}
806
807#[derive(Debug, Clone)]
809pub struct StreamRequest {
810 pub id: u64,
812 pub path: AssetPath,
814 pub type_id: TypeId,
816 pub priority: StreamPriority,
818 pub enqueued_at: Instant,
820}
821
822pub struct StreamingManager {
827 queue: Vec<StreamRequest>,
828 batch_size: usize,
830 total_processed: u64,
832}
833
834impl StreamingManager {
835 pub fn new(batch_size: usize) -> Self {
839 Self {
840 queue: Vec::new(),
841 batch_size,
842 total_processed: 0,
843 }
844 }
845
846 pub fn enqueue(&mut self, req: StreamRequest) {
851 if let Some(existing) = self.queue.iter_mut().find(|r| r.id == req.id) {
852 if req.priority > existing.priority {
853 existing.priority = req.priority;
854 }
855 return;
856 }
857 self.queue.push(req);
858 }
859
860 pub fn drain(&mut self) -> Vec<StreamRequest> {
864 if self.queue.is_empty() {
865 return Vec::new();
866 }
867 self.queue.sort_unstable_by(|a, b| {
868 b.priority
869 .cmp(&a.priority)
870 .then(a.enqueued_at.cmp(&b.enqueued_at))
871 });
872
873 let take = self.batch_size.min(self.queue.len());
874 let drained: Vec<_> = self.queue.drain(0..take).collect();
875 self.total_processed += drained.len() as u64;
876 drained
877 }
878
879 pub fn cancel(&mut self, id: u64) -> bool {
881 let before = self.queue.len();
882 self.queue.retain(|r| r.id != id);
883 self.queue.len() < before
884 }
885
886 pub fn pending(&self) -> usize {
888 self.queue.len()
889 }
890
891 pub fn total_processed(&self) -> u64 {
893 self.total_processed
894 }
895
896 pub fn is_idle(&self) -> bool {
898 self.queue.is_empty()
899 }
900}
901
902impl fmt::Debug for StreamingManager {
903 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
904 f.debug_struct("StreamingManager")
905 .field("pending", &self.queue.len())
906 .field("batch_size", &self.batch_size)
907 .field("total_processed", &self.total_processed)
908 .finish()
909 }
910}
911
912#[derive(Debug, Clone)]
918struct PackEntry {
919 virtual_path: String,
921 offset: usize,
923 length: usize,
925}
926
927pub struct AssetPack {
945 name: String,
947 entries: Vec<PackEntry>,
949 data: Vec<u8>,
951 data_offset: usize,
953}
954
955impl AssetPack {
956 pub fn from_bytes(name: impl Into<String>, bytes: Vec<u8>) -> Result<Self, String> {
960 if bytes.len() < 12 {
961 return Err("pack too small".into());
962 }
963 if &bytes[0..4] != b"PACK" {
964 return Err("invalid magic bytes".into());
965 }
966 let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
967 if version != 1 {
968 return Err(format!("unsupported pack version {version}"));
969 }
970 let entry_count = u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize;
971
972 let mut cursor = 12usize;
973 let mut entries = Vec::with_capacity(entry_count);
974
975 for _ in 0..entry_count {
976 if cursor + 4 > bytes.len() {
977 return Err("truncated directory".into());
978 }
979 let path_len = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap()) as usize;
980 cursor += 4;
981 if cursor + path_len > bytes.len() {
982 return Err("truncated path".into());
983 }
984 let virtual_path = std::str::from_utf8(&bytes[cursor..cursor + path_len])
985 .map_err(|e| format!("invalid UTF-8 in path: {e}"))?
986 .to_owned();
987 cursor += path_len;
988 if cursor + 16 > bytes.len() {
989 return Err("truncated entry offsets".into());
990 }
991 let offset = u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap()) as usize;
992 let length = u64::from_le_bytes(bytes[cursor + 8..cursor + 16].try_into().unwrap()) as usize;
993 cursor += 16;
994 entries.push(PackEntry { virtual_path, offset, length });
995 }
996
997 let data_offset = cursor;
998 Ok(Self {
999 name: name.into(),
1000 entries,
1001 data: bytes,
1002 data_offset,
1003 })
1004 }
1005
1006 pub fn build(name: impl Into<String>, files: &[(&str, &[u8])]) -> Vec<u8> {
1008 let mut dir: Vec<u8> = Vec::new();
1009 let mut blob: Vec<u8> = Vec::new();
1010
1011 dir.extend_from_slice(b"PACK");
1012 dir.extend_from_slice(&1u32.to_le_bytes());
1013 dir.extend_from_slice(&(files.len() as u32).to_le_bytes());
1014
1015 for (path, data) in files {
1016 let path_bytes = path.as_bytes();
1017 dir.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
1018 dir.extend_from_slice(path_bytes);
1019 dir.extend_from_slice(&(blob.len() as u64).to_le_bytes());
1020 dir.extend_from_slice(&(data.len() as u64).to_le_bytes());
1021 blob.extend_from_slice(data);
1022 }
1023
1024 let _ = name;
1025 let mut out = dir;
1026 out.extend_from_slice(&blob);
1027 out
1028 }
1029
1030 pub fn read(&self, virtual_path: &str) -> Option<&[u8]> {
1032 for entry in &self.entries {
1033 if entry.virtual_path == virtual_path {
1034 let start = self.data_offset + entry.offset;
1035 let end = start + entry.length;
1036 return self.data.get(start..end);
1037 }
1038 }
1039 None
1040 }
1041
1042 pub fn paths(&self) -> impl Iterator<Item = &str> {
1044 self.entries.iter().map(|e| e.virtual_path.as_str())
1045 }
1046
1047 pub fn name(&self) -> &str {
1049 &self.name
1050 }
1051
1052 pub fn entry_count(&self) -> usize {
1054 self.entries.len()
1055 }
1056}
1057
1058impl fmt::Debug for AssetPack {
1059 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1060 f.debug_struct("AssetPack")
1061 .field("name", &self.name)
1062 .field("entries", &self.entries.len())
1063 .field("total_bytes", &self.data.len())
1064 .finish()
1065 }
1066}
1067
1068#[derive(Debug, Clone)]
1074pub struct ManifestEntry {
1075 pub path: AssetPath,
1077 pub priority: StreamPriority,
1079 pub required: bool,
1081 pub tag: Option<String>,
1083}
1084
1085impl ManifestEntry {
1086 pub fn required(path: impl Into<AssetPath>) -> Self {
1088 Self {
1089 path: path.into(),
1090 priority: StreamPriority::High,
1091 required: true,
1092 tag: None,
1093 }
1094 }
1095
1096 pub fn optional(path: impl Into<AssetPath>) -> Self {
1098 Self {
1099 path: path.into(),
1100 priority: StreamPriority::Normal,
1101 required: false,
1102 tag: None,
1103 }
1104 }
1105
1106 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1108 self.tag = Some(tag.into());
1109 self
1110 }
1111
1112 pub fn with_priority(mut self, priority: StreamPriority) -> Self {
1114 self.priority = priority;
1115 self
1116 }
1117}
1118
1119#[derive(Debug, Clone)]
1133pub struct AssetManifest {
1134 pub name: String,
1136 pub entries: Vec<ManifestEntry>,
1138}
1139
1140impl AssetManifest {
1141 pub fn new(name: impl Into<String>) -> Self {
1143 Self { name: name.into(), entries: Vec::new() }
1144 }
1145
1146 pub fn add(&mut self, entry: ManifestEntry) {
1148 self.entries.push(entry);
1149 }
1150
1151 pub fn required_entries(&self) -> impl Iterator<Item = &ManifestEntry> {
1153 self.entries.iter().filter(|e| e.required)
1154 }
1155
1156 pub fn entries_with_tag<'a>(&'a self, tag: &'a str) -> impl Iterator<Item = &'a ManifestEntry> {
1158 self.entries.iter().filter(move |e| {
1159 e.tag.as_deref() == Some(tag)
1160 })
1161 }
1162
1163 pub fn len(&self) -> usize {
1165 self.entries.len()
1166 }
1167
1168 pub fn is_empty(&self) -> bool {
1170 self.entries.is_empty()
1171 }
1172}
1173
1174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1182pub enum PixelFormat {
1183 R8,
1185 Rg8,
1187 Rgb8,
1189 Rgba8,
1191 Rgba32F,
1193}
1194
1195impl PixelFormat {
1196 pub fn bytes_per_pixel(self) -> usize {
1198 match self {
1199 PixelFormat::R8 => 1,
1200 PixelFormat::Rg8 => 2,
1201 PixelFormat::Rgb8 => 3,
1202 PixelFormat::Rgba8 => 4,
1203 PixelFormat::Rgba32F => 16,
1204 }
1205 }
1206}
1207
1208#[derive(Debug, Clone)]
1212pub struct ImageAsset {
1213 pub width: u32,
1215 pub height: u32,
1217 pub format: PixelFormat,
1219 pub data: Vec<u8>,
1221 pub mip_levels: Vec<Vec<u8>>,
1223}
1224
1225impl ImageAsset {
1226 pub fn solid_color(width: u32, height: u32, rgba: [u8; 4]) -> Self {
1228 let pixels = (width * height) as usize;
1229 let mut data = Vec::with_capacity(pixels * 4);
1230 for _ in 0..pixels {
1231 data.extend_from_slice(&rgba);
1232 }
1233 Self {
1234 width,
1235 height,
1236 format: PixelFormat::Rgba8,
1237 data,
1238 mip_levels: Vec::new(),
1239 }
1240 }
1241
1242 pub fn byte_size(&self) -> usize {
1244 self.data.len()
1245 }
1246
1247 pub fn channels(&self) -> usize {
1249 self.format.bytes_per_pixel()
1250 }
1251}
1252
1253impl Asset for ImageAsset {}
1254
1255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1259pub enum ShaderStage {
1260 Vertex,
1262 Fragment,
1264 Compute,
1266 Geometry,
1268}
1269
1270impl fmt::Display for ShaderStage {
1271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1272 match self {
1273 ShaderStage::Vertex => write!(f, "vertex"),
1274 ShaderStage::Fragment => write!(f, "fragment"),
1275 ShaderStage::Compute => write!(f, "compute"),
1276 ShaderStage::Geometry => write!(f, "geometry"),
1277 }
1278 }
1279}
1280
1281#[derive(Debug, Clone)]
1283pub struct ShaderAsset {
1284 pub name: String,
1286 pub source: String,
1288 pub stage: ShaderStage,
1290 pub defines: Vec<(String, String)>,
1292}
1293
1294impl ShaderAsset {
1295 pub fn new(name: impl Into<String>, source: impl Into<String>, stage: ShaderStage) -> Self {
1297 Self {
1298 name: name.into(),
1299 source: source.into(),
1300 stage,
1301 defines: Vec::new(),
1302 }
1303 }
1304
1305 pub fn with_define(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1307 self.defines.push((key.into(), value.into()));
1308 self
1309 }
1310
1311 pub fn line_count(&self) -> usize {
1313 self.source.lines().count()
1314 }
1315}
1316
1317impl Asset for ShaderAsset {}
1318
1319#[derive(Debug, Clone, Copy)]
1323pub struct FontMetrics {
1324 pub cap_height: f32,
1326 pub ascender: f32,
1328 pub descender: f32,
1330 pub line_gap: f32,
1332 pub units_per_em: f32,
1334}
1335
1336impl Default for FontMetrics {
1337 fn default() -> Self {
1338 Self {
1339 cap_height: 700.0,
1340 ascender: 800.0,
1341 descender: -200.0,
1342 line_gap: 0.0,
1343 units_per_em: 1000.0,
1344 }
1345 }
1346}
1347
1348#[derive(Debug, Clone)]
1350pub struct GlyphData {
1351 pub codepoint: char,
1353 pub advance_width: f32,
1355 pub bounds: (f32, f32, f32, f32),
1357 pub atlas_uv: Option<(f32, f32, f32, f32)>,
1359 pub outline: Vec<OutlineCommand>,
1361}
1362
1363#[derive(Debug, Clone, Copy)]
1365pub enum OutlineCommand {
1366 MoveTo(f32, f32),
1368 LineTo(f32, f32),
1370 QuadTo(f32, f32, f32, f32),
1372 Close,
1374}
1375
1376#[derive(Debug, Clone)]
1378pub struct FontAsset {
1379 pub name: String,
1381 pub glyphs: HashMap<char, GlyphData>,
1383 pub metrics: FontMetrics,
1385 pub atlas: Option<ImageAsset>,
1387}
1388
1389impl FontAsset {
1390 pub fn glyph(&mut self, ch: char) -> Option<&GlyphData> {
1394 if self.glyphs.contains_key(&ch) {
1395 return self.glyphs.get(&ch);
1396 }
1397 if let Some(fallback) = self.glyphs.get(&'?').cloned() {
1400 self.glyphs.insert(ch, fallback);
1401 return None;
1402 }
1403 None
1404 }
1405
1406 pub fn glyph_count(&self) -> usize {
1408 self.glyphs.len()
1409 }
1410}
1411
1412impl Asset for FontAsset {}
1413
1414#[derive(Debug, Clone)]
1418pub struct SoundAsset {
1419 pub sample_rate: u32,
1421 pub channels: u16,
1423 pub samples: Vec<f32>,
1425 pub loop_start: Option<usize>,
1427 pub loop_end: Option<usize>,
1429}
1430
1431impl SoundAsset {
1432 pub fn duration_secs(&self) -> f32 {
1434 if self.sample_rate == 0 || self.channels == 0 {
1435 return 0.0;
1436 }
1437 self.samples.len() as f32 / (self.sample_rate as f32 * self.channels as f32)
1438 }
1439
1440 pub fn frame_count(&self) -> usize {
1442 if self.channels == 0 { 0 } else { self.samples.len() / self.channels as usize }
1443 }
1444}
1445
1446impl Asset for SoundAsset {}
1447
1448#[derive(Debug, Clone)]
1454pub struct ScriptAsset {
1455 pub name: String,
1457 pub source: String,
1459 pub language: Option<String>,
1461}
1462
1463impl ScriptAsset {
1464 pub fn new(name: impl Into<String>, source: impl Into<String>) -> Self {
1466 Self {
1467 name: name.into(),
1468 source: source.into(),
1469 language: None,
1470 }
1471 }
1472
1473 pub fn line_count(&self) -> usize {
1475 self.source.lines().count()
1476 }
1477
1478 pub fn byte_len(&self) -> usize {
1480 self.source.len()
1481 }
1482}
1483
1484impl Asset for ScriptAsset {}
1485
1486#[derive(Debug, Clone, Copy, PartialEq)]
1490pub struct Vertex {
1491 pub position: [f32; 3],
1493 pub normal: [f32; 3],
1495 pub uv: [f32; 2],
1497 pub tangent: [f32; 4],
1499 pub color: [f32; 4],
1501}
1502
1503impl Default for Vertex {
1504 fn default() -> Self {
1505 Self {
1506 position: [0.0; 3],
1507 normal: [0.0, 1.0, 0.0],
1508 uv: [0.0; 2],
1509 tangent: [1.0, 0.0, 0.0, 1.0],
1510 color: [1.0; 4],
1511 }
1512 }
1513}
1514
1515#[derive(Debug, Clone)]
1517pub struct MeshAsset {
1518 pub vertices: Vec<Vertex>,
1520 pub indices: Vec<u32>,
1522 pub material: Option<String>,
1524 pub aabb: Option<([f32; 3], [f32; 3])>,
1526}
1527
1528impl MeshAsset {
1529 pub fn compute_aabb(&mut self) {
1531 if self.vertices.is_empty() {
1532 self.aabb = None;
1533 return;
1534 }
1535 let mut min = [f32::MAX; 3];
1536 let mut max = [f32::MIN; 3];
1537 for v in &self.vertices {
1538 for i in 0..3 {
1539 min[i] = min[i].min(v.position[i]);
1540 max[i] = max[i].max(v.position[i]);
1541 }
1542 }
1543 self.aabb = Some((min, max));
1544 }
1545
1546 pub fn triangle_count(&self) -> usize {
1548 self.indices.len() / 3
1549 }
1550}
1551
1552impl Asset for MeshAsset {}
1553
1554#[derive(Debug, Clone)]
1558pub struct MaterialAsset {
1559 pub albedo: Option<AssetPath>,
1561 pub normal_map: Option<AssetPath>,
1563 pub roughness: MaterialParam,
1565 pub metallic: MaterialParam,
1567 pub shader: Option<AssetPath>,
1569 pub base_color: [f32; 4],
1571 pub alpha_blend: bool,
1573 pub double_sided: bool,
1575}
1576
1577#[derive(Debug, Clone)]
1579pub enum MaterialParam {
1580 Value(f32),
1582 Texture(AssetPath),
1584}
1585
1586impl Default for MaterialAsset {
1587 fn default() -> Self {
1588 Self {
1589 albedo: None,
1590 normal_map: None,
1591 roughness: MaterialParam::Value(0.5),
1592 metallic: MaterialParam::Value(0.0),
1593 shader: None,
1594 base_color: [1.0; 4],
1595 alpha_blend: false,
1596 double_sided: false,
1597 }
1598 }
1599}
1600
1601impl Asset for MaterialAsset {}
1602
1603#[derive(Debug, Clone)]
1607pub struct SceneEntity {
1608 pub name: String,
1610 pub parent: Option<String>,
1612 pub position: [f32; 3],
1614 pub rotation: [f32; 4],
1616 pub scale: [f32; 3],
1618 pub components: HashMap<String, String>,
1620}
1621
1622impl SceneEntity {
1623 pub fn new(name: impl Into<String>) -> Self {
1625 Self {
1626 name: name.into(),
1627 parent: None,
1628 position: [0.0; 3],
1629 rotation: [0.0, 0.0, 0.0, 1.0],
1630 scale: [1.0; 3],
1631 components: HashMap::new(),
1632 }
1633 }
1634}
1635
1636#[derive(Debug, Clone)]
1638pub struct PrefabRef {
1639 pub name: String,
1641 pub prefab_path: AssetPath,
1643 pub position: [f32; 3],
1645 pub rotation: [f32; 4],
1647 pub scale: [f32; 3],
1649}
1650
1651#[derive(Debug, Clone)]
1653pub struct SceneAsset {
1654 pub name: String,
1656 pub entities: Vec<SceneEntity>,
1658 pub prefabs: Vec<PrefabRef>,
1660 pub properties: HashMap<String, String>,
1662}
1663
1664impl SceneAsset {
1665 pub fn empty(name: impl Into<String>) -> Self {
1667 Self {
1668 name: name.into(),
1669 entities: Vec::new(),
1670 prefabs: Vec::new(),
1671 properties: HashMap::new(),
1672 }
1673 }
1674
1675 pub fn find_entity(&self, name: &str) -> Option<&SceneEntity> {
1677 self.entities.iter().find(|e| e.name == name)
1678 }
1679
1680 pub fn object_count(&self) -> usize {
1682 self.entities.len() + self.prefabs.len()
1683 }
1684}
1685
1686impl Asset for SceneAsset {}
1687
1688pub struct RawImageLoader;
1702
1703impl AssetLoader<ImageAsset> for RawImageLoader {
1704 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<ImageAsset, String> {
1705 if bytes.len() < 12 {
1706 return Ok(ImageAsset::solid_color(1, 1, [255, 0, 255, 255]));
1707 }
1708 if &bytes[0..4] == b"RIMG" {
1709 let width = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
1710 let height = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
1711 let expected = (width * height * 4) as usize;
1712 if bytes.len() < 12 + expected {
1713 return Err(format!("{path}: truncated RIMG data"));
1714 }
1715 return Ok(ImageAsset {
1716 width,
1717 height,
1718 format: PixelFormat::Rgba8,
1719 data: bytes[12..12 + expected].to_vec(),
1720 mip_levels: Vec::new(),
1721 });
1722 }
1723 Ok(ImageAsset::solid_color(1, 1, [255, 0, 255, 255]))
1724 }
1725
1726 fn extensions(&self) -> &[&str] {
1727 &["rimg", "png", "jpg", "jpeg", "bmp", "tga"]
1728 }
1729}
1730
1731pub struct PlainTextShaderLoader;
1737
1738impl AssetLoader<ShaderAsset> for PlainTextShaderLoader {
1739 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<ShaderAsset, String> {
1740 let source = std::str::from_utf8(bytes)
1741 .map_err(|e| format!("{path}: invalid UTF-8: {e}"))?
1742 .to_owned();
1743
1744 let stage = match path.extension().as_deref() {
1745 Some("vert") => ShaderStage::Vertex,
1746 Some("frag") => ShaderStage::Fragment,
1747 Some("comp") => ShaderStage::Compute,
1748 Some("geom") => ShaderStage::Geometry,
1749 _ => ShaderStage::Fragment,
1750 };
1751
1752 let name = path
1753 .path()
1754 .file_stem()
1755 .and_then(|s| s.to_str())
1756 .unwrap_or("unknown")
1757 .to_owned();
1758
1759 Ok(ShaderAsset::new(name, source, stage))
1760 }
1761
1762 fn extensions(&self) -> &[&str] {
1763 &["glsl", "vert", "frag", "comp", "geom", "wgsl", "hlsl"]
1764 }
1765}
1766
1767pub struct PlainTextScriptLoader;
1771
1772impl AssetLoader<ScriptAsset> for PlainTextScriptLoader {
1773 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<ScriptAsset, String> {
1774 let source = std::str::from_utf8(bytes)
1775 .map_err(|e| format!("{path}: invalid UTF-8: {e}"))?
1776 .to_owned();
1777 let name = path
1778 .path()
1779 .file_stem()
1780 .and_then(|s| s.to_str())
1781 .unwrap_or("script")
1782 .to_owned();
1783 let language = path.extension();
1784 Ok(ScriptAsset { name, source, language })
1785 }
1786
1787 fn extensions(&self) -> &[&str] {
1788 &["lua", "rhai", "wren", "js", "py", "script"]
1789 }
1790}
1791
1792pub struct RawSoundLoader;
1800
1801impl AssetLoader<SoundAsset> for RawSoundLoader {
1802 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<SoundAsset, String> {
1803 if bytes.len() < 12 {
1804 return Err(format!("{path}: sound file too small"));
1805 }
1806 if &bytes[0..4] != b"RSND" {
1807 return Ok(SoundAsset {
1808 sample_rate: 44100,
1809 channels: 1,
1810 samples: vec![0.0f32; 44100],
1811 loop_start: None,
1812 loop_end: None,
1813 });
1814 }
1815 let sample_rate = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
1816 let channels = u16::from_le_bytes(bytes[8..10].try_into().unwrap());
1817 let sample_bytes = &bytes[12..];
1818 if sample_bytes.len() % 4 != 0 {
1819 return Err(format!("{path}: sample data not aligned to 4 bytes"));
1820 }
1821 let samples: Vec<f32> = sample_bytes
1822 .chunks_exact(4)
1823 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
1824 .collect();
1825 Ok(SoundAsset { sample_rate, channels, samples, loop_start: None, loop_end: None })
1826 }
1827
1828 fn extensions(&self) -> &[&str] {
1829 &["rsnd", "wav", "ogg", "mp3", "flac"]
1830 }
1831}
1832
1833pub struct MipMapGenerator;
1842
1843impl AssetProcessor<ImageAsset> for MipMapGenerator {
1844 fn process(&self, asset: &mut ImageAsset, _path: &AssetPath) -> Result<(), String> {
1845 if asset.format != PixelFormat::Rgba8 {
1846 return Ok(());
1847 }
1848 let mut src_w = asset.width as usize;
1849 let mut src_h = asset.height as usize;
1850 let mut src_data = asset.data.clone();
1851
1852 while src_w > 1 || src_h > 1 {
1853 let dst_w = (src_w / 2).max(1);
1854 let dst_h = (src_h / 2).max(1);
1855 let mut dst_data = vec![0u8; dst_w * dst_h * 4];
1856
1857 for y in 0..dst_h {
1858 for x in 0..dst_w {
1859 let src_x = (x * 2).min(src_w - 1);
1860 let src_y = (y * 2).min(src_h - 1);
1861 let nx = (src_x + 1).min(src_w - 1);
1862 let ny = (src_y + 1).min(src_h - 1);
1863
1864 let p = |py: usize, px: usize| -> [u8; 4] {
1865 let off = (py * src_w + px) * 4;
1866 src_data[off..off + 4].try_into().unwrap()
1867 };
1868 let p00 = p(src_y, src_x);
1869 let p01 = p(src_y, nx);
1870 let p10 = p(ny, src_x);
1871 let p11 = p(ny, nx);
1872
1873 let off = (y * dst_w + x) * 4;
1874 for c in 0..4 {
1875 dst_data[off + c] = (
1876 (p00[c] as u32 + p01[c] as u32 + p10[c] as u32 + p11[c] as u32) / 4
1877 ) as u8;
1878 }
1879 }
1880 }
1881
1882 asset.mip_levels.push(dst_data.clone());
1883 src_data = dst_data;
1884 src_w = dst_w;
1885 src_h = dst_h;
1886 }
1887 Ok(())
1888 }
1889
1890 fn name(&self) -> &str {
1891 "MipMapGenerator"
1892 }
1893}
1894
1895pub struct AudioNormalizer;
1897
1898impl AssetProcessor<SoundAsset> for AudioNormalizer {
1899 fn process(&self, asset: &mut SoundAsset, _path: &AssetPath) -> Result<(), String> {
1900 let peak = asset.samples.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
1901 if peak > 0.0 && (peak - 1.0).abs() > 1e-6 {
1902 for s in &mut asset.samples {
1903 *s /= peak;
1904 }
1905 }
1906 Ok(())
1907 }
1908
1909 fn name(&self) -> &str {
1910 "AudioNormalizer"
1911 }
1912}
1913
1914#[derive(Debug, Clone)]
1920pub struct AssetServerConfig {
1921 pub root_dir: PathBuf,
1923 pub cache_capacity: usize,
1925 pub stream_batch_size: usize,
1927 pub hot_reload: bool,
1929 pub hot_reload_interval: Duration,
1931}
1932
1933impl Default for AssetServerConfig {
1934 fn default() -> Self {
1935 Self {
1936 root_dir: PathBuf::from("assets"),
1937 cache_capacity: 512,
1938 stream_batch_size: 8,
1939 hot_reload: cfg!(debug_assertions),
1940 hot_reload_interval: Duration::from_secs(1),
1941 }
1942 }
1943}
1944
1945type ErasedLoadFn = Box<dyn Fn(&[u8], &AssetPath) -> Result<Box<dyn Any + Send + Sync>, String> + Send + Sync>;
1947
1948type ErasedStoreFn = Box<dyn Fn(&mut AssetRegistry, u64, Box<dyn Any + Send + Sync>, Option<SystemTime>) + Send + Sync>;
1950
1951struct LoaderEntry {
1952 extensions: Vec<String>,
1953 type_id: TypeId,
1954 load: ErasedLoadFn,
1955 store: ErasedStoreFn,
1956}
1957
1958pub struct AssetServer {
1967 config: AssetServerConfig,
1968 registry: AssetRegistry,
1969 cache: AssetCache,
1970 hot_reload: HotReload,
1971 streaming: StreamingManager,
1972 loaders: Vec<LoaderEntry>,
1973 packs: Vec<AssetPack>,
1974 typed_slots: HashMap<u64, Box<dyn Any + Send + Sync>>,
1976 stats: AssetServerStats,
1978}
1979
1980#[derive(Debug, Clone, Default)]
1982pub struct AssetServerStats {
1983 pub loads_from_disk: u64,
1985 pub loads_from_pack: u64,
1987 pub bytes_read: u64,
1989 pub evictions: u64,
1991 pub hot_reloads: u64,
1993 pub failures: u64,
1995}
1996
1997impl AssetServer {
1998 pub fn new_with_config(config: AssetServerConfig) -> Self {
2000 let cache = AssetCache::new(config.cache_capacity);
2001 let hot_reload = HotReload::new(config.hot_reload_interval, config.hot_reload);
2002 let streaming = StreamingManager::new(config.stream_batch_size);
2003 Self {
2004 config,
2005 registry: AssetRegistry::new(),
2006 cache,
2007 hot_reload,
2008 streaming,
2009 loaders: Vec::new(),
2010 packs: Vec::new(),
2011 typed_slots: HashMap::new(),
2012 stats: AssetServerStats::default(),
2013 }
2014 }
2015
2016 pub fn new() -> Self {
2018 Self::new_with_config(AssetServerConfig::default())
2019 }
2020
2021 pub fn set_root_dir(&mut self, dir: impl Into<PathBuf>) {
2023 self.config.root_dir = dir.into();
2024 }
2025
2026 pub fn register_loader<A: Asset, L: AssetLoader<A>>(&mut self, loader: L) {
2032 let extensions: Vec<String> = loader.extensions().iter().map(|e| e.to_string()).collect();
2033 let type_id = TypeId::of::<A>();
2034 let loader = Arc::new(loader);
2035
2036 let load_loader = Arc::clone(&loader);
2037 let load: ErasedLoadFn = Box::new(move |bytes, path| {
2038 load_loader
2039 .load(bytes, path)
2040 .map(|a| Box::new(a) as Box<dyn Any + Send + Sync>)
2041 });
2042
2043 let store: ErasedStoreFn = Box::new(|registry, id, boxed, mtime| {
2044 if let Ok(asset) = boxed.downcast::<A>() {
2045 registry.store::<A>(id, *asset, mtime);
2046 }
2047 });
2048
2049 self.loaders.push(LoaderEntry { extensions, type_id, load, store });
2050 }
2051
2052 pub fn mount_pack(&mut self, pack: AssetPack) {
2054 self.packs.push(pack);
2055 }
2056
2057 pub fn load<A: Asset>(&mut self, path: impl Into<AssetPath>) -> AssetHandle<A> {
2064 let path = path.into();
2065 let type_id = TypeId::of::<A>();
2066
2067 if let Some(id) = self.registry.id_for_path(&path) {
2068 return self.make_handle::<A>(id);
2069 }
2070
2071 let id = self.registry.alloc(type_id, path.clone());
2072 self.registry.mark_loading(id);
2073
2074 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2075 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2076
2077 self.streaming.enqueue(StreamRequest {
2078 id,
2079 path,
2080 type_id,
2081 priority: StreamPriority::Normal,
2082 enqueued_at: Instant::now(),
2083 });
2084
2085 AssetHandle::new(AssetId::new(id), arc)
2086 }
2087
2088 pub fn load_with_priority<A: Asset>(
2090 &mut self,
2091 path: impl Into<AssetPath>,
2092 priority: StreamPriority,
2093 ) -> AssetHandle<A> {
2094 let path = path.into();
2095 let type_id = TypeId::of::<A>();
2096
2097 if let Some(id) = self.registry.id_for_path(&path) {
2098 return self.make_handle::<A>(id);
2099 }
2100
2101 let id = self.registry.alloc(type_id, path.clone());
2102 self.registry.mark_loading(id);
2103
2104 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2105 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2106
2107 self.streaming.enqueue(StreamRequest {
2108 id,
2109 path,
2110 type_id,
2111 priority,
2112 enqueued_at: Instant::now(),
2113 });
2114
2115 AssetHandle::new(AssetId::new(id), arc)
2116 }
2117
2118 pub fn load_manifest(&mut self, manifest: &AssetManifest) {
2124 for entry in &manifest.entries {
2125 let path = entry.path.clone();
2126 let priority = entry.priority;
2127
2128 if self.registry.id_for_path(&path).is_none() {
2129 let id = self.registry.alloc(TypeId::of::<ScriptAsset>(), path.clone());
2131 self.registry.mark_loading(id);
2132 self.streaming.enqueue(StreamRequest {
2133 id,
2134 path,
2135 type_id: TypeId::of::<ScriptAsset>(),
2136 priority,
2137 enqueued_at: Instant::now(),
2138 });
2139 }
2140 }
2141 }
2142
2143 pub fn update(&mut self) {
2148 let batch = self.streaming.drain();
2149 for req in batch {
2150 self.execute_load(req);
2151 }
2152
2153 let changed = self.hot_reload.poll();
2154 for (_path, asset_ids) in changed {
2155 for id in asset_ids {
2156 self.enqueue_reload(id);
2157 }
2158 }
2159 }
2160
2161 pub fn get<A: Asset>(&mut self, handle: &AssetHandle<A>) -> Option<Arc<A>> {
2165 let id = handle.id().raw();
2166 let arc = self.registry.get::<A>(id)?;
2167
2168 if let Some(evict_id) = self.cache.touch(id) {
2169 self.registry.evict(evict_id);
2170 self.cache.remove(evict_id);
2171 self.typed_slots.remove(&evict_id);
2172 self.stats.evictions += 1;
2173 }
2174
2175 Some(arc)
2176 }
2177
2178 pub fn load_state<A: Asset>(&self, handle: &AssetHandle<A>) -> LoadState {
2180 self.registry.load_state(handle.id().raw())
2181 }
2182
2183 pub fn reload<A: Asset>(&mut self, handle: &AssetHandle<A>) {
2185 let id = handle.id().raw();
2186 self.enqueue_reload(id);
2187 let batch = self.streaming.drain();
2189 for req in batch {
2190 self.execute_load(req);
2191 }
2192 }
2193
2194 pub fn insert<A: Asset>(&mut self, path: impl Into<AssetPath>, asset: A) -> AssetHandle<A> {
2199 let path = path.into();
2200 let type_id = TypeId::of::<A>();
2201
2202 let id = if let Some(existing_id) = self.registry.id_for_path(&path) {
2203 existing_id
2204 } else {
2205 self.registry.alloc(type_id, path)
2206 };
2207
2208 self.registry.store::<A>(id, asset, None);
2209
2210 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2212 if let Some(value_arc) = self.registry.get::<A>(id) {
2214 let _ = value_arc;
2220 }
2221 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2222
2223 if let Some(evict_id) = self.cache.touch(id) {
2224 self.registry.evict(evict_id);
2225 self.cache.remove(evict_id);
2226 self.typed_slots.remove(&evict_id);
2227 self.stats.evictions += 1;
2228 }
2229
2230 AssetHandle::new(AssetId::new(id), arc)
2231 }
2232
2233 pub fn stats(&self) -> &AssetServerStats {
2235 &self.stats
2236 }
2237
2238 pub fn asset_count(&self) -> usize {
2240 self.registry.len()
2241 }
2242
2243 pub fn is_idle(&self) -> bool {
2245 self.streaming.is_idle()
2246 }
2247
2248 pub fn read_bytes(&mut self, path: &AssetPath) -> Result<(Vec<u8>, Option<SystemTime>), String> {
2251 let virtual_str = path.path().to_string_lossy().replace('\\', "/");
2252
2253 for pack in &self.packs {
2254 if let Some(data) = pack.read(&virtual_str) {
2255 self.stats.loads_from_pack += 1;
2256 self.stats.bytes_read += data.len() as u64;
2257 return Ok((data.to_vec(), None));
2258 }
2259 }
2260
2261 let full_path = self.config.root_dir.join(path.path());
2262 let mtime = std::fs::metadata(&full_path)
2263 .and_then(|m| m.modified())
2264 .ok();
2265 let data = std::fs::read(&full_path)
2266 .map_err(|e| format!("failed to read {}: {e}", full_path.display()))?;
2267 self.stats.loads_from_disk += 1;
2268 self.stats.bytes_read += data.len() as u64;
2269 Ok((data, mtime))
2270 }
2271
2272 fn make_handle<A: Asset>(&mut self, id: u64) -> AssetHandle<A> {
2276 if let Some(boxed) = self.typed_slots.get(&id) {
2278 if let Some(arc) = boxed.downcast_ref::<Arc<RwLock<Option<A>>>>() {
2279 return AssetHandle::new(AssetId::new(id), Arc::clone(arc));
2280 }
2281 }
2282 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2284 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2285 AssetHandle::new(AssetId::new(id), arc)
2286 }
2287
2288 fn execute_load(&mut self, req: StreamRequest) {
2290 let path = req.path.clone();
2291 let id = req.id;
2292 let ext = path.extension().unwrap_or_default();
2293
2294 let loader_idx = self.loaders.iter().rposition(|l| {
2296 l.type_id == req.type_id && l.extensions.iter().any(|e| e == &ext)
2297 }).or_else(|| {
2298 self.loaders.iter().rposition(|l| l.type_id == req.type_id)
2300 });
2301
2302 let loader_idx = match loader_idx {
2303 Some(i) => i,
2304 None => {
2305 let msg = format!("no loader for extension '{ext}'");
2306 self.registry.mark_failed(id, msg);
2307 self.stats.failures += 1;
2308 return;
2309 }
2310 };
2311
2312 let (bytes, mtime) = match self.read_bytes(&path) {
2313 Ok(b) => b,
2314 Err(e) => {
2315 self.registry.mark_failed(id, e);
2316 self.stats.failures += 1;
2317 return;
2318 }
2319 };
2320
2321 let loaded = (self.loaders[loader_idx].load)(&bytes, &path);
2322 match loaded {
2323 Ok(boxed) => {
2324 (self.loaders[loader_idx].store)(&mut self.registry, id, boxed, mtime);
2325 if mtime.is_some() {
2326 let disk_path = self.config.root_dir.join(path.path());
2327 self.hot_reload.watch(disk_path, id, mtime);
2328 }
2329 if let Some(evict_id) = self.cache.touch(id) {
2330 self.registry.evict(evict_id);
2331 self.cache.remove(evict_id);
2332 self.typed_slots.remove(&evict_id);
2333 self.stats.evictions += 1;
2334 }
2335 }
2336 Err(msg) => {
2337 self.registry.mark_failed(id, msg);
2338 self.stats.failures += 1;
2339 }
2340 }
2341 }
2342
2343 fn enqueue_reload(&mut self, id: u64) {
2345 let path_opt = self.registry.path_for_id(id).cloned();
2346 let type_id_opt = self.registry.type_id_for(id);
2347 if let (Some(path), Some(type_id)) = (path_opt, type_id_opt) {
2348 self.registry.mark_loading(id);
2349 self.streaming.enqueue(StreamRequest {
2350 id,
2351 path,
2352 type_id,
2353 priority: StreamPriority::High,
2354 enqueued_at: Instant::now(),
2355 });
2356 self.stats.hot_reloads += 1;
2357 }
2358 }
2359}
2360
2361impl Default for AssetServer {
2362 fn default() -> Self {
2363 Self::new()
2364 }
2365}
2366
2367impl fmt::Debug for AssetServer {
2368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2369 f.debug_struct("AssetServer")
2370 .field("assets", &self.registry.len())
2371 .field("pending", &self.streaming.pending())
2372 .field("cache", &self.cache)
2373 .field("hot_reload", &self.hot_reload)
2374 .field("stats", &self.stats)
2375 .finish()
2376 }
2377}
2378
2379pub fn default_asset_server() -> AssetServer {
2393 let mut server = AssetServer::new();
2394 server.register_loader::<ImageAsset, _>(RawImageLoader);
2395 server.register_loader::<ShaderAsset, _>(PlainTextShaderLoader);
2396 server.register_loader::<ScriptAsset, _>(PlainTextScriptLoader);
2397 server.register_loader::<SoundAsset, _>(RawSoundLoader);
2398 server
2399}
2400
2401pub fn load_file_bytes(path: &Path) -> Result<Vec<u8>, String> {
2405 std::fs::read(path).map_err(|e| format!("load_file_bytes: {e}"))
2406}
2407
2408#[cfg(test)]
2413mod tests {
2414 use super::*;
2415
2416 #[test]
2419 fn asset_path_parse_no_label() {
2420 let p = AssetPath::parse("textures/player.png");
2421 assert_eq!(p.path(), Path::new("textures/player.png"));
2422 assert_eq!(p.label(), None);
2423 }
2424
2425 #[test]
2426 fn asset_path_parse_with_label() {
2427 let p = AssetPath::parse("models/robot.gltf#body");
2428 assert_eq!(p.path(), Path::new("models/robot.gltf"));
2429 assert_eq!(p.label(), Some("body"));
2430 }
2431
2432 #[test]
2433 fn asset_path_extension() {
2434 let p = AssetPath::new("shaders/main.frag");
2435 assert_eq!(p.extension(), Some("frag".to_string()));
2436 }
2437
2438 #[test]
2439 fn asset_path_display() {
2440 let p = AssetPath::with_label("a/b.png", "sub");
2441 assert!(p.to_string().contains('#'));
2442 }
2443
2444 #[test]
2445 fn asset_path_from_str() {
2446 let p: AssetPath = "foo/bar.lua".into();
2447 assert_eq!(p.extension(), Some("lua".to_string()));
2448 }
2449
2450 #[test]
2453 fn asset_id_equality() {
2454 let a = AssetId::<ImageAsset>::new(42);
2455 let b = AssetId::<ImageAsset>::new(42);
2456 let c = AssetId::<ImageAsset>::new(7);
2457 assert_eq!(a, b);
2458 assert_ne!(a, c);
2459 }
2460
2461 #[test]
2462 fn asset_id_copy() {
2463 let a = AssetId::<ShaderAsset>::new(1);
2464 let b = a;
2465 assert_eq!(a, b);
2466 }
2467
2468 #[test]
2471 fn load_state_display() {
2472 assert_eq!(LoadState::NotLoaded.to_string(), "NotLoaded");
2473 assert_eq!(LoadState::Loading.to_string(), "Loading");
2474 assert_eq!(LoadState::Loaded.to_string(), "Loaded");
2475 assert!(LoadState::Failed("oops".into()).to_string().contains("oops"));
2476 }
2477
2478 #[test]
2479 fn load_state_equality() {
2480 assert_eq!(LoadState::Loaded, LoadState::Loaded);
2481 assert_ne!(LoadState::Loaded, LoadState::Loading);
2482 assert_eq!(
2483 LoadState::Failed("x".into()),
2484 LoadState::Failed("x".into())
2485 );
2486 }
2487
2488 #[test]
2491 fn registry_alloc_and_lookup() {
2492 let mut reg = AssetRegistry::new();
2493 let path = AssetPath::new("test.png");
2494 let id = reg.alloc(TypeId::of::<ImageAsset>(), path.clone());
2495 assert_eq!(reg.id_for_path(&path), Some(id));
2496 assert_eq!(reg.load_state(id), LoadState::NotLoaded);
2497 }
2498
2499 #[test]
2500 fn registry_store_and_get() {
2501 let mut reg = AssetRegistry::new();
2502 let path = AssetPath::new("solid.png");
2503 let id = reg.alloc(TypeId::of::<ImageAsset>(), path);
2504 let img = ImageAsset::solid_color(4, 4, [0, 0, 0, 255]);
2505 reg.store::<ImageAsset>(id, img, None);
2506 assert_eq!(reg.load_state(id), LoadState::Loaded);
2507 let arc = reg.get::<ImageAsset>(id).unwrap();
2508 assert_eq!(arc.width, 4);
2509 }
2510
2511 #[test]
2512 fn registry_mark_failed() {
2513 let mut reg = AssetRegistry::new();
2514 let id = reg.alloc(TypeId::of::<ImageAsset>(), AssetPath::new("x.png"));
2515 reg.mark_failed(id, "disk error".into());
2516 assert!(matches!(reg.load_state(id), LoadState::Failed(_)));
2517 }
2518
2519 #[test]
2520 fn registry_evict() {
2521 let mut reg = AssetRegistry::new();
2522 let id = reg.alloc(TypeId::of::<ImageAsset>(), AssetPath::new("e.png"));
2523 assert!(reg.evict(id));
2524 assert_eq!(reg.load_state(id), LoadState::NotLoaded);
2525 assert!(!reg.evict(id));
2526 }
2527
2528 #[test]
2531 fn cache_lru_eviction() {
2532 let mut cache = AssetCache::new(3);
2533 assert_eq!(cache.touch(1), None);
2534 assert_eq!(cache.touch(2), None);
2535 assert_eq!(cache.touch(3), None);
2536 let evicted = cache.touch(4);
2537 assert_eq!(evicted, Some(1));
2538 }
2539
2540 #[test]
2541 fn cache_touch_updates_order() {
2542 let mut cache = AssetCache::new(3);
2543 cache.touch(1);
2544 cache.touch(2);
2545 cache.touch(3);
2546 cache.touch(1); let evicted = cache.touch(4);
2548 assert_eq!(evicted, Some(2));
2549 }
2550
2551 #[test]
2552 fn cache_unlimited() {
2553 let mut cache = AssetCache::new(0);
2554 for i in 0..1000u64 {
2555 assert_eq!(cache.touch(i), None);
2556 }
2557 assert_eq!(cache.len(), 1000);
2558 }
2559
2560 #[test]
2561 fn cache_set_capacity_evicts() {
2562 let mut cache = AssetCache::new(10);
2563 for i in 0..10u64 {
2564 cache.touch(i);
2565 }
2566 let evicted = cache.set_capacity(5);
2567 assert_eq!(evicted.len(), 5);
2568 assert_eq!(cache.len(), 5);
2569 }
2570
2571 #[test]
2574 fn hot_reload_disabled_returns_empty() {
2575 let mut hr = HotReload::new(Duration::from_secs(1), false);
2576 hr.watch(PathBuf::from("x.png"), 1, None);
2577 let changed = hr.poll();
2578 assert!(changed.is_empty());
2579 }
2580
2581 #[test]
2582 fn hot_reload_watch_count() {
2583 let mut hr = HotReload::new(Duration::from_secs(60), true);
2584 hr.watch(PathBuf::from("a.png"), 1, None);
2585 hr.watch(PathBuf::from("b.png"), 2, None);
2586 assert_eq!(hr.watched_count(), 2);
2587 }
2588
2589 #[test]
2590 fn hot_reload_unwatch() {
2591 let mut hr = HotReload::new(Duration::from_secs(60), true);
2592 hr.watch(PathBuf::from("a.png"), 1, None);
2593 hr.unwatch(1);
2594 assert_eq!(hr.watched_count(), 0);
2595 }
2596
2597 #[test]
2600 fn streaming_priority_order() {
2601 let mut sm = StreamingManager::new(10);
2602 let make = |id: u64, priority: StreamPriority| StreamRequest {
2603 id,
2604 path: AssetPath::new("x"),
2605 type_id: TypeId::of::<ImageAsset>(),
2606 priority,
2607 enqueued_at: Instant::now(),
2608 };
2609 sm.enqueue(make(1, StreamPriority::Low));
2610 sm.enqueue(make(2, StreamPriority::Critical));
2611 sm.enqueue(make(3, StreamPriority::Normal));
2612
2613 let drained = sm.drain();
2614 assert_eq!(drained[0].id, 2);
2615 assert_eq!(drained[1].id, 3);
2616 assert_eq!(drained[2].id, 1);
2617 }
2618
2619 #[test]
2620 fn streaming_cancel() {
2621 let mut sm = StreamingManager::new(10);
2622 sm.enqueue(StreamRequest {
2623 id: 99,
2624 path: AssetPath::new("y"),
2625 type_id: TypeId::of::<ImageAsset>(),
2626 priority: StreamPriority::Normal,
2627 enqueued_at: Instant::now(),
2628 });
2629 assert_eq!(sm.pending(), 1);
2630 assert!(sm.cancel(99));
2631 assert_eq!(sm.pending(), 0);
2632 }
2633
2634 #[test]
2635 fn streaming_batch_limit() {
2636 let mut sm = StreamingManager::new(2);
2637 for i in 0..5u64 {
2638 sm.enqueue(StreamRequest {
2639 id: i,
2640 path: AssetPath::new("z"),
2641 type_id: TypeId::of::<ImageAsset>(),
2642 priority: StreamPriority::Normal,
2643 enqueued_at: Instant::now(),
2644 });
2645 }
2646 let first = sm.drain();
2647 assert_eq!(first.len(), 2);
2648 assert_eq!(sm.pending(), 3);
2649 }
2650
2651 #[test]
2654 fn pack_build_and_read() {
2655 let files: &[(&str, &[u8])] = &[
2656 ("shaders/main.vert", b"void main() {}"),
2657 ("textures/logo.png", &[0u8, 1, 2, 3, 4]),
2658 ];
2659 let bytes = AssetPack::build("test", files);
2660 let pack = AssetPack::from_bytes("test", bytes).expect("parse failed");
2661 assert_eq!(pack.entry_count(), 2);
2662 assert_eq!(pack.read("shaders/main.vert"), Some(b"void main() {}".as_ref()));
2663 assert_eq!(pack.read("textures/logo.png"), Some([0u8, 1, 2, 3, 4].as_ref()));
2664 assert_eq!(pack.read("nonexistent"), None);
2665 }
2666
2667 #[test]
2668 fn pack_invalid_magic() {
2669 let result = AssetPack::from_bytes("bad", b"XXXX\0\0\0\0".to_vec());
2670 assert!(result.is_err());
2671 }
2672
2673 #[test]
2674 fn pack_paths_iterator() {
2675 let files: &[(&str, &[u8])] = &[("a.txt", b"hello"), ("b.txt", b"world")];
2676 let bytes = AssetPack::build("p", files);
2677 let pack = AssetPack::from_bytes("p", bytes).unwrap();
2678 let paths: Vec<_> = pack.paths().collect();
2679 assert!(paths.contains(&"a.txt"));
2680 assert!(paths.contains(&"b.txt"));
2681 }
2682
2683 #[test]
2686 fn manifest_required_optional() {
2687 let mut m = AssetManifest::new("level1");
2688 m.add(ManifestEntry::required("textures/floor.png"));
2689 m.add(ManifestEntry::optional("sounds/bg.wav"));
2690 assert_eq!(m.len(), 2);
2691 assert_eq!(m.required_entries().count(), 1);
2692 }
2693
2694 #[test]
2695 fn manifest_tag_filter() {
2696 let mut m = AssetManifest::new("lvl");
2697 m.add(ManifestEntry::required("a.png").with_tag("ui"));
2698 m.add(ManifestEntry::optional("b.png").with_tag("world"));
2699 m.add(ManifestEntry::optional("c.png").with_tag("ui"));
2700 assert_eq!(m.entries_with_tag("ui").count(), 2);
2701 assert_eq!(m.entries_with_tag("world").count(), 1);
2702 }
2703
2704 #[test]
2707 fn image_solid_color() {
2708 let img = ImageAsset::solid_color(2, 2, [255, 0, 0, 255]);
2709 assert_eq!(img.width, 2);
2710 assert_eq!(img.height, 2);
2711 assert_eq!(img.data.len(), 16);
2712 assert_eq!(img.data[0], 255);
2713 assert_eq!(img.data[1], 0);
2714 }
2715
2716 #[test]
2717 fn mipmap_processor() {
2718 let img = ImageAsset::solid_color(4, 4, [128, 64, 32, 255]);
2719 let mut img = img;
2720 let proc = MipMapGenerator;
2721 proc.process(&mut img, &AssetPath::new("test.png")).unwrap();
2722 assert_eq!(img.mip_levels.len(), 2);
2724 assert_eq!(img.mip_levels[0].len(), 2 * 2 * 4); }
2726
2727 #[test]
2730 fn sound_duration() {
2731 let snd = SoundAsset {
2732 sample_rate: 44100,
2733 channels: 1,
2734 samples: vec![0.0f32; 44100],
2735 loop_start: None,
2736 loop_end: None,
2737 };
2738 assert!((snd.duration_secs() - 1.0).abs() < 1e-4);
2739 }
2740
2741 #[test]
2742 fn audio_normalizer() {
2743 let mut snd = SoundAsset {
2744 sample_rate: 44100,
2745 channels: 1,
2746 samples: vec![0.5f32, -0.5, 0.25],
2747 loop_start: None,
2748 loop_end: None,
2749 };
2750 let norm = AudioNormalizer;
2751 norm.process(&mut snd, &AssetPath::new("test.wav")).unwrap();
2752 assert!((snd.samples[0] - 1.0).abs() < 1e-5);
2753 }
2754
2755 #[test]
2758 fn shader_line_count() {
2759 let src = "void main() {\n gl_Position = vec4(0);\n}\n";
2760 let shader = ShaderAsset::new("test", src, ShaderStage::Vertex);
2761 assert_eq!(shader.line_count(), 3);
2762 }
2763
2764 #[test]
2767 fn script_byte_len() {
2768 let s = ScriptAsset::new("test", "print('hello')");
2769 assert_eq!(s.byte_len(), 14);
2770 }
2771
2772 #[test]
2775 fn mesh_aabb() {
2776 let mut mesh = MeshAsset {
2777 vertices: vec![
2778 Vertex { position: [-1.0, 0.0, 0.0], ..Default::default() },
2779 Vertex { position: [1.0, 2.0, 3.0], ..Default::default() },
2780 ],
2781 indices: vec![0, 1, 0],
2782 material: None,
2783 aabb: None,
2784 };
2785 mesh.compute_aabb();
2786 let (min, max) = mesh.aabb.unwrap();
2787 assert_eq!(min, [-1.0, 0.0, 0.0]);
2788 assert_eq!(max, [1.0, 2.0, 3.0]);
2789 }
2790
2791 #[test]
2792 fn mesh_triangle_count() {
2793 let mesh = MeshAsset {
2794 vertices: vec![Vertex::default(); 3],
2795 indices: vec![0, 1, 2, 0, 2, 1],
2796 material: None,
2797 aabb: None,
2798 };
2799 assert_eq!(mesh.triangle_count(), 2);
2800 }
2801
2802 #[test]
2805 fn scene_find_entity() {
2806 let mut scene = SceneAsset::empty("test_scene");
2807 scene.entities.push(SceneEntity::new("player"));
2808 assert!(scene.find_entity("player").is_some());
2809 assert!(scene.find_entity("enemy").is_none());
2810 }
2811
2812 #[test]
2815 fn server_insert_and_load_state() {
2816 let mut server = AssetServer::new();
2817 server.register_loader::<ImageAsset, _>(RawImageLoader);
2818
2819 let img = ImageAsset::solid_color(8, 8, [0, 255, 0, 255]);
2820 let handle = server.insert::<ImageAsset>("generated/green.png", img);
2821
2822 assert_eq!(server.load_state(&handle), LoadState::Loaded);
2823 }
2824
2825 #[test]
2826 fn server_default_asset_server() {
2827 let server = default_asset_server();
2828 assert_eq!(server.asset_count(), 0);
2829 assert!(server.is_idle());
2830 }
2831
2832 #[test]
2833 fn server_pack_load() {
2834 let files: &[(&str, &[u8])] = &[("shaders/quad.vert", b"// vert shader")];
2835 let pack_bytes = AssetPack::build("shaders", files);
2836 let pack = AssetPack::from_bytes("shaders", pack_bytes).unwrap();
2837
2838 let mut server = AssetServer::new_with_config(AssetServerConfig {
2839 root_dir: PathBuf::from("nonexistent"),
2840 ..Default::default()
2841 });
2842 server.register_loader::<ShaderAsset, _>(PlainTextShaderLoader);
2843 server.mount_pack(pack);
2844
2845 let result = server.read_bytes(&AssetPath::new("shaders/quad.vert"));
2846 assert!(result.is_ok());
2847 let (data, _) = result.unwrap();
2848 assert_eq!(data, b"// vert shader");
2849 }
2850
2851 #[test]
2852 fn server_load_enqueues_request() {
2853 let mut server = AssetServer::new();
2854 server.register_loader::<ImageAsset, _>(RawImageLoader);
2855 let _handle = server.load::<ImageAsset>(AssetPath::new("test.png"));
2856 assert!(!server.is_idle());
2858 }
2859
2860 #[test]
2861 fn server_get_returns_none_before_update() {
2862 let mut server = AssetServer::new();
2863 server.register_loader::<ImageAsset, _>(RawImageLoader);
2864 let handle = server.load::<ImageAsset>(AssetPath::new("test.png"));
2865 assert!(server.get(&handle).is_none());
2867 }
2868
2869 #[test]
2870 fn streaming_priority_upgrade() {
2871 let mut sm = StreamingManager::new(10);
2872 sm.enqueue(StreamRequest {
2873 id: 1,
2874 path: AssetPath::new("a.png"),
2875 type_id: TypeId::of::<ImageAsset>(),
2876 priority: StreamPriority::Low,
2877 enqueued_at: Instant::now(),
2878 });
2879 sm.enqueue(StreamRequest {
2881 id: 1,
2882 path: AssetPath::new("a.png"),
2883 type_id: TypeId::of::<ImageAsset>(),
2884 priority: StreamPriority::Critical,
2885 enqueued_at: Instant::now(),
2886 });
2887 assert_eq!(sm.pending(), 1);
2888 let drained = sm.drain();
2889 assert_eq!(drained[0].priority, StreamPriority::Critical);
2890 }
2891
2892 #[test]
2893 fn pixel_format_bytes_per_pixel() {
2894 assert_eq!(PixelFormat::R8.bytes_per_pixel(), 1);
2895 assert_eq!(PixelFormat::Rgba8.bytes_per_pixel(), 4);
2896 assert_eq!(PixelFormat::Rgba32F.bytes_per_pixel(), 16);
2897 }
2898
2899 #[test]
2900 fn font_asset_glyph_fallback() {
2901 let mut font = FontAsset {
2902 name: "test".into(),
2903 glyphs: HashMap::new(),
2904 metrics: FontMetrics::default(),
2905 atlas: None,
2906 };
2907 font.glyphs.insert('?', GlyphData {
2908 codepoint: '?',
2909 advance_width: 500.0,
2910 bounds: (0.0, 0.0, 500.0, 700.0),
2911 atlas_uv: None,
2912 outline: Vec::new(),
2913 });
2914 assert!(font.glyph('A').is_none()); assert!(font.glyph('A').is_some());
2917 assert_eq!(font.glyph('A').unwrap().advance_width, 500.0);
2918 }
2919
2920 #[test]
2921 fn material_param_variants() {
2922 let m = MaterialAsset::default();
2923 assert!(matches!(m.roughness, MaterialParam::Value(_)));
2924 let tex = MaterialParam::Texture(AssetPath::new("rough.png"));
2925 assert!(matches!(tex, MaterialParam::Texture(_)));
2926 }
2927}