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;
1705
1706impl AssetLoader<ImageAsset> for RawImageLoader {
1707 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<ImageAsset, String> {
1708 if bytes.len() >= 12 && &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 as usize) * (height as usize) * 4;
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 let (width, height, data) = crate::export::load_rgba(bytes)
1724 .map_err(|e| format!("{path}: {e}"))?;
1725 Ok(ImageAsset { width, height, format: PixelFormat::Rgba8, data, mip_levels: Vec::new() })
1726 }
1727
1728 fn extensions(&self) -> &[&str] {
1729 &["rimg", "png", "jpg", "jpeg", "bmp", "tga", "gif"]
1730 }
1731}
1732
1733pub struct PlainTextShaderLoader;
1739
1740impl AssetLoader<ShaderAsset> for PlainTextShaderLoader {
1741 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<ShaderAsset, String> {
1742 let source = std::str::from_utf8(bytes)
1743 .map_err(|e| format!("{path}: invalid UTF-8: {e}"))?
1744 .to_owned();
1745
1746 let stage = match path.extension().as_deref() {
1747 Some("vert") => ShaderStage::Vertex,
1748 Some("frag") => ShaderStage::Fragment,
1749 Some("comp") => ShaderStage::Compute,
1750 Some("geom") => ShaderStage::Geometry,
1751 _ => ShaderStage::Fragment,
1752 };
1753
1754 let name = path
1755 .path()
1756 .file_stem()
1757 .and_then(|s| s.to_str())
1758 .unwrap_or("unknown")
1759 .to_owned();
1760
1761 Ok(ShaderAsset::new(name, source, stage))
1762 }
1763
1764 fn extensions(&self) -> &[&str] {
1765 &["glsl", "vert", "frag", "comp", "geom", "wgsl", "hlsl"]
1766 }
1767}
1768
1769pub struct PlainTextScriptLoader;
1773
1774impl AssetLoader<ScriptAsset> for PlainTextScriptLoader {
1775 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<ScriptAsset, String> {
1776 let source = std::str::from_utf8(bytes)
1777 .map_err(|e| format!("{path}: invalid UTF-8: {e}"))?
1778 .to_owned();
1779 let name = path
1780 .path()
1781 .file_stem()
1782 .and_then(|s| s.to_str())
1783 .unwrap_or("script")
1784 .to_owned();
1785 let language = path.extension();
1786 Ok(ScriptAsset { name, source, language })
1787 }
1788
1789 fn extensions(&self) -> &[&str] {
1790 &["lua", "rhai", "wren", "js", "py", "script"]
1791 }
1792}
1793
1794pub struct RawSoundLoader;
1807
1808impl AssetLoader<SoundAsset> for RawSoundLoader {
1809 fn load(&self, bytes: &[u8], path: &AssetPath) -> Result<SoundAsset, String> {
1810 if bytes.len() >= 4 && &bytes[0..4] == b"RIFF" {
1811 let clip = crate::audio::wav::read_wav(bytes).map_err(|e| format!("{path}: {e}"))?;
1812 return Ok(SoundAsset {
1813 sample_rate: clip.sample_rate,
1814 channels: clip.channels,
1815 samples: clip.samples,
1816 loop_start: None,
1817 loop_end: None,
1818 });
1819 }
1820 if bytes.len() < 12 || &bytes[0..4] != b"RSND" {
1821 return Err(format!("{path}: not a WAV or RSND file"));
1822 }
1823 let sample_rate = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
1824 let channels = u16::from_le_bytes(bytes[8..10].try_into().unwrap());
1825 let sample_bytes = &bytes[12..];
1826 if sample_bytes.len() % 4 != 0 {
1827 return Err(format!("{path}: sample data not aligned to 4 bytes"));
1828 }
1829 let samples: Vec<f32> = sample_bytes
1830 .chunks_exact(4)
1831 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
1832 .collect();
1833 Ok(SoundAsset { sample_rate, channels, samples, loop_start: None, loop_end: None })
1834 }
1835
1836 fn extensions(&self) -> &[&str] {
1837 &["rsnd", "wav"]
1838 }
1839}
1840
1841pub struct MipMapGenerator;
1850
1851impl AssetProcessor<ImageAsset> for MipMapGenerator {
1852 fn process(&self, asset: &mut ImageAsset, _path: &AssetPath) -> Result<(), String> {
1853 if asset.format != PixelFormat::Rgba8 {
1854 return Ok(());
1855 }
1856 let mut src_w = asset.width as usize;
1857 let mut src_h = asset.height as usize;
1858 let mut src_data = asset.data.clone();
1859
1860 while src_w > 1 || src_h > 1 {
1861 let dst_w = (src_w / 2).max(1);
1862 let dst_h = (src_h / 2).max(1);
1863 let mut dst_data = vec![0u8; dst_w * dst_h * 4];
1864
1865 for y in 0..dst_h {
1866 for x in 0..dst_w {
1867 let src_x = (x * 2).min(src_w - 1);
1868 let src_y = (y * 2).min(src_h - 1);
1869 let nx = (src_x + 1).min(src_w - 1);
1870 let ny = (src_y + 1).min(src_h - 1);
1871
1872 let p = |py: usize, px: usize| -> [u8; 4] {
1873 let off = (py * src_w + px) * 4;
1874 src_data[off..off + 4].try_into().unwrap()
1875 };
1876 let p00 = p(src_y, src_x);
1877 let p01 = p(src_y, nx);
1878 let p10 = p(ny, src_x);
1879 let p11 = p(ny, nx);
1880
1881 let off = (y * dst_w + x) * 4;
1882 for c in 0..4 {
1883 dst_data[off + c] = (
1884 (p00[c] as u32 + p01[c] as u32 + p10[c] as u32 + p11[c] as u32) / 4
1885 ) as u8;
1886 }
1887 }
1888 }
1889
1890 asset.mip_levels.push(dst_data.clone());
1891 src_data = dst_data;
1892 src_w = dst_w;
1893 src_h = dst_h;
1894 }
1895 Ok(())
1896 }
1897
1898 fn name(&self) -> &str {
1899 "MipMapGenerator"
1900 }
1901}
1902
1903pub struct AudioNormalizer;
1905
1906impl AssetProcessor<SoundAsset> for AudioNormalizer {
1907 fn process(&self, asset: &mut SoundAsset, _path: &AssetPath) -> Result<(), String> {
1908 let peak = asset.samples.iter().copied().map(f32::abs).fold(0.0f32, f32::max);
1909 if peak > 0.0 && (peak - 1.0).abs() > 1e-6 {
1910 for s in &mut asset.samples {
1911 *s /= peak;
1912 }
1913 }
1914 Ok(())
1915 }
1916
1917 fn name(&self) -> &str {
1918 "AudioNormalizer"
1919 }
1920}
1921
1922#[derive(Debug, Clone)]
1928pub struct AssetServerConfig {
1929 pub root_dir: PathBuf,
1931 pub cache_capacity: usize,
1933 pub stream_batch_size: usize,
1935 pub hot_reload: bool,
1937 pub hot_reload_interval: Duration,
1939}
1940
1941impl Default for AssetServerConfig {
1942 fn default() -> Self {
1943 Self {
1944 root_dir: PathBuf::from("assets"),
1945 cache_capacity: 512,
1946 stream_batch_size: 8,
1947 hot_reload: cfg!(debug_assertions),
1948 hot_reload_interval: Duration::from_secs(1),
1949 }
1950 }
1951}
1952
1953type ErasedLoadFn = Box<dyn Fn(&[u8], &AssetPath) -> Result<Box<dyn Any + Send + Sync>, String> + Send + Sync>;
1955
1956type ErasedStoreFn = Box<dyn Fn(&mut AssetRegistry, u64, Box<dyn Any + Send + Sync>, Option<SystemTime>) + Send + Sync>;
1958
1959struct LoaderEntry {
1960 extensions: Vec<String>,
1961 type_id: TypeId,
1962 load: ErasedLoadFn,
1963 store: ErasedStoreFn,
1964}
1965
1966pub struct AssetServer {
1975 config: AssetServerConfig,
1976 registry: AssetRegistry,
1977 cache: AssetCache,
1978 hot_reload: HotReload,
1979 streaming: StreamingManager,
1980 loaders: Vec<LoaderEntry>,
1981 packs: Vec<AssetPack>,
1982 typed_slots: HashMap<u64, Box<dyn Any + Send + Sync>>,
1984 stats: AssetServerStats,
1986}
1987
1988#[derive(Debug, Clone, Default)]
1990pub struct AssetServerStats {
1991 pub loads_from_disk: u64,
1993 pub loads_from_pack: u64,
1995 pub bytes_read: u64,
1997 pub evictions: u64,
1999 pub hot_reloads: u64,
2001 pub failures: u64,
2003}
2004
2005impl AssetServer {
2006 pub fn new_with_config(config: AssetServerConfig) -> Self {
2008 let cache = AssetCache::new(config.cache_capacity);
2009 let hot_reload = HotReload::new(config.hot_reload_interval, config.hot_reload);
2010 let streaming = StreamingManager::new(config.stream_batch_size);
2011 Self {
2012 config,
2013 registry: AssetRegistry::new(),
2014 cache,
2015 hot_reload,
2016 streaming,
2017 loaders: Vec::new(),
2018 packs: Vec::new(),
2019 typed_slots: HashMap::new(),
2020 stats: AssetServerStats::default(),
2021 }
2022 }
2023
2024 pub fn new() -> Self {
2026 Self::new_with_config(AssetServerConfig::default())
2027 }
2028
2029 pub fn set_root_dir(&mut self, dir: impl Into<PathBuf>) {
2031 self.config.root_dir = dir.into();
2032 }
2033
2034 pub fn register_loader<A: Asset, L: AssetLoader<A>>(&mut self, loader: L) {
2040 let extensions: Vec<String> = loader.extensions().iter().map(|e| e.to_string()).collect();
2041 let type_id = TypeId::of::<A>();
2042 let loader = Arc::new(loader);
2043
2044 let load_loader = Arc::clone(&loader);
2045 let load: ErasedLoadFn = Box::new(move |bytes, path| {
2046 load_loader
2047 .load(bytes, path)
2048 .map(|a| Box::new(a) as Box<dyn Any + Send + Sync>)
2049 });
2050
2051 let store: ErasedStoreFn = Box::new(|registry, id, boxed, mtime| {
2052 if let Ok(asset) = boxed.downcast::<A>() {
2053 registry.store::<A>(id, *asset, mtime);
2054 }
2055 });
2056
2057 self.loaders.push(LoaderEntry { extensions, type_id, load, store });
2058 }
2059
2060 pub fn mount_pack(&mut self, pack: AssetPack) {
2062 self.packs.push(pack);
2063 }
2064
2065 pub fn load<A: Asset>(&mut self, path: impl Into<AssetPath>) -> AssetHandle<A> {
2072 let path = path.into();
2073 let type_id = TypeId::of::<A>();
2074
2075 if let Some(id) = self.registry.id_for_path(&path) {
2076 return self.make_handle::<A>(id);
2077 }
2078
2079 let id = self.registry.alloc(type_id, path.clone());
2080 self.registry.mark_loading(id);
2081
2082 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2083 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2084
2085 self.streaming.enqueue(StreamRequest {
2086 id,
2087 path,
2088 type_id,
2089 priority: StreamPriority::Normal,
2090 enqueued_at: Instant::now(),
2091 });
2092
2093 AssetHandle::new(AssetId::new(id), arc)
2094 }
2095
2096 pub fn load_with_priority<A: Asset>(
2098 &mut self,
2099 path: impl Into<AssetPath>,
2100 priority: StreamPriority,
2101 ) -> AssetHandle<A> {
2102 let path = path.into();
2103 let type_id = TypeId::of::<A>();
2104
2105 if let Some(id) = self.registry.id_for_path(&path) {
2106 return self.make_handle::<A>(id);
2107 }
2108
2109 let id = self.registry.alloc(type_id, path.clone());
2110 self.registry.mark_loading(id);
2111
2112 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2113 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2114
2115 self.streaming.enqueue(StreamRequest {
2116 id,
2117 path,
2118 type_id,
2119 priority,
2120 enqueued_at: Instant::now(),
2121 });
2122
2123 AssetHandle::new(AssetId::new(id), arc)
2124 }
2125
2126 pub fn load_manifest(&mut self, manifest: &AssetManifest) {
2132 for entry in &manifest.entries {
2133 let path = entry.path.clone();
2134 let priority = entry.priority;
2135
2136 if self.registry.id_for_path(&path).is_none() {
2137 let id = self.registry.alloc(TypeId::of::<ScriptAsset>(), path.clone());
2139 self.registry.mark_loading(id);
2140 self.streaming.enqueue(StreamRequest {
2141 id,
2142 path,
2143 type_id: TypeId::of::<ScriptAsset>(),
2144 priority,
2145 enqueued_at: Instant::now(),
2146 });
2147 }
2148 }
2149 }
2150
2151 pub fn update(&mut self) {
2156 let batch = self.streaming.drain();
2157 for req in batch {
2158 self.execute_load(req);
2159 }
2160
2161 let changed = self.hot_reload.poll();
2162 for (_path, asset_ids) in changed {
2163 for id in asset_ids {
2164 self.enqueue_reload(id);
2165 }
2166 }
2167 }
2168
2169 pub fn get<A: Asset>(&mut self, handle: &AssetHandle<A>) -> Option<Arc<A>> {
2173 let id = handle.id().raw();
2174 let arc = self.registry.get::<A>(id)?;
2175
2176 if let Some(evict_id) = self.cache.touch(id) {
2177 self.registry.evict(evict_id);
2178 self.cache.remove(evict_id);
2179 self.typed_slots.remove(&evict_id);
2180 self.stats.evictions += 1;
2181 }
2182
2183 Some(arc)
2184 }
2185
2186 pub fn load_state<A: Asset>(&self, handle: &AssetHandle<A>) -> LoadState {
2188 self.registry.load_state(handle.id().raw())
2189 }
2190
2191 pub fn reload<A: Asset>(&mut self, handle: &AssetHandle<A>) {
2193 let id = handle.id().raw();
2194 self.enqueue_reload(id);
2195 let batch = self.streaming.drain();
2197 for req in batch {
2198 self.execute_load(req);
2199 }
2200 }
2201
2202 pub fn insert<A: Asset>(&mut self, path: impl Into<AssetPath>, asset: A) -> AssetHandle<A> {
2207 let path = path.into();
2208 let type_id = TypeId::of::<A>();
2209
2210 let id = if let Some(existing_id) = self.registry.id_for_path(&path) {
2211 existing_id
2212 } else {
2213 self.registry.alloc(type_id, path)
2214 };
2215
2216 self.registry.store::<A>(id, asset, None);
2217
2218 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2220 if let Some(value_arc) = self.registry.get::<A>(id) {
2222 let _ = value_arc;
2228 }
2229 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2230
2231 if let Some(evict_id) = self.cache.touch(id) {
2232 self.registry.evict(evict_id);
2233 self.cache.remove(evict_id);
2234 self.typed_slots.remove(&evict_id);
2235 self.stats.evictions += 1;
2236 }
2237
2238 AssetHandle::new(AssetId::new(id), arc)
2239 }
2240
2241 pub fn stats(&self) -> &AssetServerStats {
2243 &self.stats
2244 }
2245
2246 pub fn asset_count(&self) -> usize {
2248 self.registry.len()
2249 }
2250
2251 pub fn is_idle(&self) -> bool {
2253 self.streaming.is_idle()
2254 }
2255
2256 pub fn read_bytes(&mut self, path: &AssetPath) -> Result<(Vec<u8>, Option<SystemTime>), String> {
2259 let virtual_str = path.path().to_string_lossy().replace('\\', "/");
2260
2261 for pack in &self.packs {
2262 if let Some(data) = pack.read(&virtual_str) {
2263 self.stats.loads_from_pack += 1;
2264 self.stats.bytes_read += data.len() as u64;
2265 return Ok((data.to_vec(), None));
2266 }
2267 }
2268
2269 let full_path = self.config.root_dir.join(path.path());
2270 let mtime = std::fs::metadata(&full_path)
2271 .and_then(|m| m.modified())
2272 .ok();
2273 let data = std::fs::read(&full_path)
2274 .map_err(|e| format!("failed to read {}: {e}", full_path.display()))?;
2275 self.stats.loads_from_disk += 1;
2276 self.stats.bytes_read += data.len() as u64;
2277 Ok((data, mtime))
2278 }
2279
2280 fn make_handle<A: Asset>(&mut self, id: u64) -> AssetHandle<A> {
2284 if let Some(boxed) = self.typed_slots.get(&id) {
2286 if let Some(arc) = boxed.downcast_ref::<Arc<RwLock<Option<A>>>>() {
2287 return AssetHandle::new(AssetId::new(id), Arc::clone(arc));
2288 }
2289 }
2290 let arc: Arc<RwLock<Option<A>>> = Arc::new(RwLock::new(None));
2292 self.typed_slots.insert(id, Box::new(Arc::clone(&arc)));
2293 AssetHandle::new(AssetId::new(id), arc)
2294 }
2295
2296 fn execute_load(&mut self, req: StreamRequest) {
2298 let path = req.path.clone();
2299 let id = req.id;
2300 let ext = path.extension().unwrap_or_default();
2301
2302 let loader_idx = self.loaders.iter().rposition(|l| {
2304 l.type_id == req.type_id && l.extensions.iter().any(|e| e == &ext)
2305 }).or_else(|| {
2306 self.loaders.iter().rposition(|l| l.type_id == req.type_id)
2308 });
2309
2310 let loader_idx = match loader_idx {
2311 Some(i) => i,
2312 None => {
2313 let msg = format!("no loader for extension '{ext}'");
2314 self.registry.mark_failed(id, msg);
2315 self.stats.failures += 1;
2316 return;
2317 }
2318 };
2319
2320 let (bytes, mtime) = match self.read_bytes(&path) {
2321 Ok(b) => b,
2322 Err(e) => {
2323 self.registry.mark_failed(id, e);
2324 self.stats.failures += 1;
2325 return;
2326 }
2327 };
2328
2329 let loaded = (self.loaders[loader_idx].load)(&bytes, &path);
2330 match loaded {
2331 Ok(boxed) => {
2332 (self.loaders[loader_idx].store)(&mut self.registry, id, boxed, mtime);
2333 if mtime.is_some() {
2334 let disk_path = self.config.root_dir.join(path.path());
2335 self.hot_reload.watch(disk_path, id, mtime);
2336 }
2337 if let Some(evict_id) = self.cache.touch(id) {
2338 self.registry.evict(evict_id);
2339 self.cache.remove(evict_id);
2340 self.typed_slots.remove(&evict_id);
2341 self.stats.evictions += 1;
2342 }
2343 }
2344 Err(msg) => {
2345 self.registry.mark_failed(id, msg);
2346 self.stats.failures += 1;
2347 }
2348 }
2349 }
2350
2351 fn enqueue_reload(&mut self, id: u64) {
2353 let path_opt = self.registry.path_for_id(id).cloned();
2354 let type_id_opt = self.registry.type_id_for(id);
2355 if let (Some(path), Some(type_id)) = (path_opt, type_id_opt) {
2356 self.registry.mark_loading(id);
2357 self.streaming.enqueue(StreamRequest {
2358 id,
2359 path,
2360 type_id,
2361 priority: StreamPriority::High,
2362 enqueued_at: Instant::now(),
2363 });
2364 self.stats.hot_reloads += 1;
2365 }
2366 }
2367}
2368
2369impl Default for AssetServer {
2370 fn default() -> Self {
2371 Self::new()
2372 }
2373}
2374
2375impl fmt::Debug for AssetServer {
2376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2377 f.debug_struct("AssetServer")
2378 .field("assets", &self.registry.len())
2379 .field("pending", &self.streaming.pending())
2380 .field("cache", &self.cache)
2381 .field("hot_reload", &self.hot_reload)
2382 .field("stats", &self.stats)
2383 .finish()
2384 }
2385}
2386
2387pub fn default_asset_server() -> AssetServer {
2401 let mut server = AssetServer::new();
2402 server.register_loader::<ImageAsset, _>(RawImageLoader);
2403 server.register_loader::<ShaderAsset, _>(PlainTextShaderLoader);
2404 server.register_loader::<ScriptAsset, _>(PlainTextScriptLoader);
2405 server.register_loader::<SoundAsset, _>(RawSoundLoader);
2406 server
2407}
2408
2409pub fn load_file_bytes(path: &Path) -> Result<Vec<u8>, String> {
2413 std::fs::read(path).map_err(|e| format!("load_file_bytes: {e}"))
2414}
2415
2416#[cfg(test)]
2421mod tests {
2422 use super::*;
2423
2424 #[test]
2427 fn asset_path_parse_no_label() {
2428 let p = AssetPath::parse("textures/player.png");
2429 assert_eq!(p.path(), Path::new("textures/player.png"));
2430 assert_eq!(p.label(), None);
2431 }
2432
2433 #[test]
2434 fn asset_path_parse_with_label() {
2435 let p = AssetPath::parse("models/robot.gltf#body");
2436 assert_eq!(p.path(), Path::new("models/robot.gltf"));
2437 assert_eq!(p.label(), Some("body"));
2438 }
2439
2440 #[test]
2441 fn asset_path_extension() {
2442 let p = AssetPath::new("shaders/main.frag");
2443 assert_eq!(p.extension(), Some("frag".to_string()));
2444 }
2445
2446 #[test]
2447 fn asset_path_display() {
2448 let p = AssetPath::with_label("a/b.png", "sub");
2449 assert!(p.to_string().contains('#'));
2450 }
2451
2452 #[test]
2453 fn asset_path_from_str() {
2454 let p: AssetPath = "foo/bar.lua".into();
2455 assert_eq!(p.extension(), Some("lua".to_string()));
2456 }
2457
2458 #[test]
2461 fn asset_id_equality() {
2462 let a = AssetId::<ImageAsset>::new(42);
2463 let b = AssetId::<ImageAsset>::new(42);
2464 let c = AssetId::<ImageAsset>::new(7);
2465 assert_eq!(a, b);
2466 assert_ne!(a, c);
2467 }
2468
2469 #[test]
2470 fn asset_id_copy() {
2471 let a = AssetId::<ShaderAsset>::new(1);
2472 let b = a;
2473 assert_eq!(a, b);
2474 }
2475
2476 #[test]
2479 fn load_state_display() {
2480 assert_eq!(LoadState::NotLoaded.to_string(), "NotLoaded");
2481 assert_eq!(LoadState::Loading.to_string(), "Loading");
2482 assert_eq!(LoadState::Loaded.to_string(), "Loaded");
2483 assert!(LoadState::Failed("oops".into()).to_string().contains("oops"));
2484 }
2485
2486 #[test]
2487 fn load_state_equality() {
2488 assert_eq!(LoadState::Loaded, LoadState::Loaded);
2489 assert_ne!(LoadState::Loaded, LoadState::Loading);
2490 assert_eq!(
2491 LoadState::Failed("x".into()),
2492 LoadState::Failed("x".into())
2493 );
2494 }
2495
2496 #[test]
2499 fn registry_alloc_and_lookup() {
2500 let mut reg = AssetRegistry::new();
2501 let path = AssetPath::new("test.png");
2502 let id = reg.alloc(TypeId::of::<ImageAsset>(), path.clone());
2503 assert_eq!(reg.id_for_path(&path), Some(id));
2504 assert_eq!(reg.load_state(id), LoadState::NotLoaded);
2505 }
2506
2507 #[test]
2508 fn registry_store_and_get() {
2509 let mut reg = AssetRegistry::new();
2510 let path = AssetPath::new("solid.png");
2511 let id = reg.alloc(TypeId::of::<ImageAsset>(), path);
2512 let img = ImageAsset::solid_color(4, 4, [0, 0, 0, 255]);
2513 reg.store::<ImageAsset>(id, img, None);
2514 assert_eq!(reg.load_state(id), LoadState::Loaded);
2515 let arc = reg.get::<ImageAsset>(id).unwrap();
2516 assert_eq!(arc.width, 4);
2517 }
2518
2519 #[test]
2520 fn registry_mark_failed() {
2521 let mut reg = AssetRegistry::new();
2522 let id = reg.alloc(TypeId::of::<ImageAsset>(), AssetPath::new("x.png"));
2523 reg.mark_failed(id, "disk error".into());
2524 assert!(matches!(reg.load_state(id), LoadState::Failed(_)));
2525 }
2526
2527 #[test]
2528 fn registry_evict() {
2529 let mut reg = AssetRegistry::new();
2530 let id = reg.alloc(TypeId::of::<ImageAsset>(), AssetPath::new("e.png"));
2531 assert!(reg.evict(id));
2532 assert_eq!(reg.load_state(id), LoadState::NotLoaded);
2533 assert!(!reg.evict(id));
2534 }
2535
2536 #[test]
2539 fn cache_lru_eviction() {
2540 let mut cache = AssetCache::new(3);
2541 assert_eq!(cache.touch(1), None);
2542 assert_eq!(cache.touch(2), None);
2543 assert_eq!(cache.touch(3), None);
2544 let evicted = cache.touch(4);
2545 assert_eq!(evicted, Some(1));
2546 }
2547
2548 #[test]
2549 fn cache_touch_updates_order() {
2550 let mut cache = AssetCache::new(3);
2551 cache.touch(1);
2552 cache.touch(2);
2553 cache.touch(3);
2554 cache.touch(1); let evicted = cache.touch(4);
2556 assert_eq!(evicted, Some(2));
2557 }
2558
2559 #[test]
2560 fn cache_unlimited() {
2561 let mut cache = AssetCache::new(0);
2562 for i in 0..1000u64 {
2563 assert_eq!(cache.touch(i), None);
2564 }
2565 assert_eq!(cache.len(), 1000);
2566 }
2567
2568 #[test]
2569 fn cache_set_capacity_evicts() {
2570 let mut cache = AssetCache::new(10);
2571 for i in 0..10u64 {
2572 cache.touch(i);
2573 }
2574 let evicted = cache.set_capacity(5);
2575 assert_eq!(evicted.len(), 5);
2576 assert_eq!(cache.len(), 5);
2577 }
2578
2579 #[test]
2582 fn hot_reload_disabled_returns_empty() {
2583 let mut hr = HotReload::new(Duration::from_secs(1), false);
2584 hr.watch(PathBuf::from("x.png"), 1, None);
2585 let changed = hr.poll();
2586 assert!(changed.is_empty());
2587 }
2588
2589 #[test]
2590 fn hot_reload_watch_count() {
2591 let mut hr = HotReload::new(Duration::from_secs(60), true);
2592 hr.watch(PathBuf::from("a.png"), 1, None);
2593 hr.watch(PathBuf::from("b.png"), 2, None);
2594 assert_eq!(hr.watched_count(), 2);
2595 }
2596
2597 #[test]
2598 fn hot_reload_unwatch() {
2599 let mut hr = HotReload::new(Duration::from_secs(60), true);
2600 hr.watch(PathBuf::from("a.png"), 1, None);
2601 hr.unwatch(1);
2602 assert_eq!(hr.watched_count(), 0);
2603 }
2604
2605 #[test]
2608 fn streaming_priority_order() {
2609 let mut sm = StreamingManager::new(10);
2610 let make = |id: u64, priority: StreamPriority| StreamRequest {
2611 id,
2612 path: AssetPath::new("x"),
2613 type_id: TypeId::of::<ImageAsset>(),
2614 priority,
2615 enqueued_at: Instant::now(),
2616 };
2617 sm.enqueue(make(1, StreamPriority::Low));
2618 sm.enqueue(make(2, StreamPriority::Critical));
2619 sm.enqueue(make(3, StreamPriority::Normal));
2620
2621 let drained = sm.drain();
2622 assert_eq!(drained[0].id, 2);
2623 assert_eq!(drained[1].id, 3);
2624 assert_eq!(drained[2].id, 1);
2625 }
2626
2627 #[test]
2628 fn streaming_cancel() {
2629 let mut sm = StreamingManager::new(10);
2630 sm.enqueue(StreamRequest {
2631 id: 99,
2632 path: AssetPath::new("y"),
2633 type_id: TypeId::of::<ImageAsset>(),
2634 priority: StreamPriority::Normal,
2635 enqueued_at: Instant::now(),
2636 });
2637 assert_eq!(sm.pending(), 1);
2638 assert!(sm.cancel(99));
2639 assert_eq!(sm.pending(), 0);
2640 }
2641
2642 #[test]
2643 fn streaming_batch_limit() {
2644 let mut sm = StreamingManager::new(2);
2645 for i in 0..5u64 {
2646 sm.enqueue(StreamRequest {
2647 id: i,
2648 path: AssetPath::new("z"),
2649 type_id: TypeId::of::<ImageAsset>(),
2650 priority: StreamPriority::Normal,
2651 enqueued_at: Instant::now(),
2652 });
2653 }
2654 let first = sm.drain();
2655 assert_eq!(first.len(), 2);
2656 assert_eq!(sm.pending(), 3);
2657 }
2658
2659 #[test]
2662 fn pack_build_and_read() {
2663 let files: &[(&str, &[u8])] = &[
2664 ("shaders/main.vert", b"void main() {}"),
2665 ("textures/logo.png", &[0u8, 1, 2, 3, 4]),
2666 ];
2667 let bytes = AssetPack::build("test", files);
2668 let pack = AssetPack::from_bytes("test", bytes).expect("parse failed");
2669 assert_eq!(pack.entry_count(), 2);
2670 assert_eq!(pack.read("shaders/main.vert"), Some(b"void main() {}".as_ref()));
2671 assert_eq!(pack.read("textures/logo.png"), Some([0u8, 1, 2, 3, 4].as_ref()));
2672 assert_eq!(pack.read("nonexistent"), None);
2673 }
2674
2675 #[test]
2676 fn pack_invalid_magic() {
2677 let result = AssetPack::from_bytes("bad", b"XXXX\0\0\0\0".to_vec());
2678 assert!(result.is_err());
2679 }
2680
2681 #[test]
2682 fn pack_paths_iterator() {
2683 let files: &[(&str, &[u8])] = &[("a.txt", b"hello"), ("b.txt", b"world")];
2684 let bytes = AssetPack::build("p", files);
2685 let pack = AssetPack::from_bytes("p", bytes).unwrap();
2686 let paths: Vec<_> = pack.paths().collect();
2687 assert!(paths.contains(&"a.txt"));
2688 assert!(paths.contains(&"b.txt"));
2689 }
2690
2691 #[test]
2694 fn manifest_required_optional() {
2695 let mut m = AssetManifest::new("level1");
2696 m.add(ManifestEntry::required("textures/floor.png"));
2697 m.add(ManifestEntry::optional("sounds/bg.wav"));
2698 assert_eq!(m.len(), 2);
2699 assert_eq!(m.required_entries().count(), 1);
2700 }
2701
2702 #[test]
2703 fn manifest_tag_filter() {
2704 let mut m = AssetManifest::new("lvl");
2705 m.add(ManifestEntry::required("a.png").with_tag("ui"));
2706 m.add(ManifestEntry::optional("b.png").with_tag("world"));
2707 m.add(ManifestEntry::optional("c.png").with_tag("ui"));
2708 assert_eq!(m.entries_with_tag("ui").count(), 2);
2709 assert_eq!(m.entries_with_tag("world").count(), 1);
2710 }
2711
2712 #[test]
2715 fn image_solid_color() {
2716 let img = ImageAsset::solid_color(2, 2, [255, 0, 0, 255]);
2717 assert_eq!(img.width, 2);
2718 assert_eq!(img.height, 2);
2719 assert_eq!(img.data.len(), 16);
2720 assert_eq!(img.data[0], 255);
2721 assert_eq!(img.data[1], 0);
2722 }
2723
2724 #[test]
2725 fn mipmap_processor() {
2726 let img = ImageAsset::solid_color(4, 4, [128, 64, 32, 255]);
2727 let mut img = img;
2728 let proc = MipMapGenerator;
2729 proc.process(&mut img, &AssetPath::new("test.png")).unwrap();
2730 assert_eq!(img.mip_levels.len(), 2);
2732 assert_eq!(img.mip_levels[0].len(), 2 * 2 * 4); }
2734
2735 #[test]
2738 fn sound_duration() {
2739 let snd = SoundAsset {
2740 sample_rate: 44100,
2741 channels: 1,
2742 samples: vec![0.0f32; 44100],
2743 loop_start: None,
2744 loop_end: None,
2745 };
2746 assert!((snd.duration_secs() - 1.0).abs() < 1e-4);
2747 }
2748
2749 #[test]
2750 fn audio_normalizer() {
2751 let mut snd = SoundAsset {
2752 sample_rate: 44100,
2753 channels: 1,
2754 samples: vec![0.5f32, -0.5, 0.25],
2755 loop_start: None,
2756 loop_end: None,
2757 };
2758 let norm = AudioNormalizer;
2759 norm.process(&mut snd, &AssetPath::new("test.wav")).unwrap();
2760 assert!((snd.samples[0] - 1.0).abs() < 1e-5);
2761 }
2762
2763 #[test]
2766 fn shader_line_count() {
2767 let src = "void main() {\n gl_Position = vec4(0);\n}\n";
2768 let shader = ShaderAsset::new("test", src, ShaderStage::Vertex);
2769 assert_eq!(shader.line_count(), 3);
2770 }
2771
2772 #[test]
2775 fn script_byte_len() {
2776 let s = ScriptAsset::new("test", "print('hello')");
2777 assert_eq!(s.byte_len(), 14);
2778 }
2779
2780 #[test]
2783 fn mesh_aabb() {
2784 let mut mesh = MeshAsset {
2785 vertices: vec![
2786 Vertex { position: [-1.0, 0.0, 0.0], ..Default::default() },
2787 Vertex { position: [1.0, 2.0, 3.0], ..Default::default() },
2788 ],
2789 indices: vec![0, 1, 0],
2790 material: None,
2791 aabb: None,
2792 };
2793 mesh.compute_aabb();
2794 let (min, max) = mesh.aabb.unwrap();
2795 assert_eq!(min, [-1.0, 0.0, 0.0]);
2796 assert_eq!(max, [1.0, 2.0, 3.0]);
2797 }
2798
2799 #[test]
2800 fn mesh_triangle_count() {
2801 let mesh = MeshAsset {
2802 vertices: vec![Vertex::default(); 3],
2803 indices: vec![0, 1, 2, 0, 2, 1],
2804 material: None,
2805 aabb: None,
2806 };
2807 assert_eq!(mesh.triangle_count(), 2);
2808 }
2809
2810 #[test]
2813 fn scene_find_entity() {
2814 let mut scene = SceneAsset::empty("test_scene");
2815 scene.entities.push(SceneEntity::new("player"));
2816 assert!(scene.find_entity("player").is_some());
2817 assert!(scene.find_entity("enemy").is_none());
2818 }
2819
2820 #[test]
2823 fn server_insert_and_load_state() {
2824 let mut server = AssetServer::new();
2825 server.register_loader::<ImageAsset, _>(RawImageLoader);
2826
2827 let img = ImageAsset::solid_color(8, 8, [0, 255, 0, 255]);
2828 let handle = server.insert::<ImageAsset>("generated/green.png", img);
2829
2830 assert_eq!(server.load_state(&handle), LoadState::Loaded);
2831 }
2832
2833 #[test]
2834 fn image_loader_decodes_real_png_and_rimg() {
2835 let px: Vec<u8> = vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 9, 9, 9, 128];
2836 let path = std::env::temp_dir().join("proof_engine_asset_loader.png");
2837 crate::export::save_rgba(&path, 2, 2, &px).unwrap();
2838 let bytes = std::fs::read(&path).unwrap();
2839 let img = RawImageLoader.load(&bytes, &AssetPath::new("tex.png")).unwrap();
2840 assert_eq!((img.width, img.height), (2, 2));
2841 assert_eq!(img.format, PixelFormat::Rgba8);
2842 assert_eq!(img.data, px);
2843
2844 let mut rimg = b"RIMG".to_vec();
2845 rimg.extend_from_slice(&1u32.to_le_bytes());
2846 rimg.extend_from_slice(&1u32.to_le_bytes());
2847 rimg.extend_from_slice(&[1, 2, 3, 4]);
2848 let img = RawImageLoader.load(&rimg, &AssetPath::new("tex.rimg")).unwrap();
2849 assert_eq!(img.data, vec![1, 2, 3, 4]);
2850 }
2851
2852 #[test]
2853 fn image_loader_reports_garbage_as_an_error() {
2854 let err = RawImageLoader.load(b"definitely not a png file", &AssetPath::new("bad.png"));
2855 assert!(err.is_err());
2856 }
2857
2858 #[test]
2859 fn sound_loader_reads_wav_and_refuses_unknown_formats() {
2860 let samples = [0.0f32, 0.5, -0.5, 1.0];
2861 let path = std::env::temp_dir().join("proof_engine_asset_loader.wav");
2862 crate::audio::wav::write_wav(&path, 22050, 2, &samples).unwrap();
2863 let bytes = std::fs::read(&path).unwrap();
2864 let snd = RawSoundLoader.load(&bytes, &AssetPath::new("clip.wav")).unwrap();
2865 assert_eq!((snd.sample_rate, snd.channels), (22050, 2));
2866 assert_eq!(snd.frame_count(), 2);
2867 for (a, b) in snd.samples.iter().zip(samples) {
2868 assert!((a - b).abs() < 1e-4, "{a} vs {b}");
2869 }
2870 assert!(RawSoundLoader.load(b"OggS and then some bytes", &AssetPath::new("x.ogg")).is_err());
2871 }
2872
2873 #[test]
2874 fn server_default_asset_server() {
2875 let server = default_asset_server();
2876 assert_eq!(server.asset_count(), 0);
2877 assert!(server.is_idle());
2878 }
2879
2880 #[test]
2881 fn server_pack_load() {
2882 let files: &[(&str, &[u8])] = &[("shaders/quad.vert", b"// vert shader")];
2883 let pack_bytes = AssetPack::build("shaders", files);
2884 let pack = AssetPack::from_bytes("shaders", pack_bytes).unwrap();
2885
2886 let mut server = AssetServer::new_with_config(AssetServerConfig {
2887 root_dir: PathBuf::from("nonexistent"),
2888 ..Default::default()
2889 });
2890 server.register_loader::<ShaderAsset, _>(PlainTextShaderLoader);
2891 server.mount_pack(pack);
2892
2893 let result = server.read_bytes(&AssetPath::new("shaders/quad.vert"));
2894 assert!(result.is_ok());
2895 let (data, _) = result.unwrap();
2896 assert_eq!(data, b"// vert shader");
2897 }
2898
2899 #[test]
2900 fn server_load_enqueues_request() {
2901 let mut server = AssetServer::new();
2902 server.register_loader::<ImageAsset, _>(RawImageLoader);
2903 let _handle = server.load::<ImageAsset>(AssetPath::new("test.png"));
2904 assert!(!server.is_idle());
2906 }
2907
2908 #[test]
2909 fn server_get_returns_none_before_update() {
2910 let mut server = AssetServer::new();
2911 server.register_loader::<ImageAsset, _>(RawImageLoader);
2912 let handle = server.load::<ImageAsset>(AssetPath::new("test.png"));
2913 assert!(server.get(&handle).is_none());
2915 }
2916
2917 #[test]
2918 fn streaming_priority_upgrade() {
2919 let mut sm = StreamingManager::new(10);
2920 sm.enqueue(StreamRequest {
2921 id: 1,
2922 path: AssetPath::new("a.png"),
2923 type_id: TypeId::of::<ImageAsset>(),
2924 priority: StreamPriority::Low,
2925 enqueued_at: Instant::now(),
2926 });
2927 sm.enqueue(StreamRequest {
2929 id: 1,
2930 path: AssetPath::new("a.png"),
2931 type_id: TypeId::of::<ImageAsset>(),
2932 priority: StreamPriority::Critical,
2933 enqueued_at: Instant::now(),
2934 });
2935 assert_eq!(sm.pending(), 1);
2936 let drained = sm.drain();
2937 assert_eq!(drained[0].priority, StreamPriority::Critical);
2938 }
2939
2940 #[test]
2941 fn pixel_format_bytes_per_pixel() {
2942 assert_eq!(PixelFormat::R8.bytes_per_pixel(), 1);
2943 assert_eq!(PixelFormat::Rgba8.bytes_per_pixel(), 4);
2944 assert_eq!(PixelFormat::Rgba32F.bytes_per_pixel(), 16);
2945 }
2946
2947 #[test]
2948 fn font_asset_glyph_fallback() {
2949 let mut font = FontAsset {
2950 name: "test".into(),
2951 glyphs: HashMap::new(),
2952 metrics: FontMetrics::default(),
2953 atlas: None,
2954 };
2955 font.glyphs.insert('?', GlyphData {
2956 codepoint: '?',
2957 advance_width: 500.0,
2958 bounds: (0.0, 0.0, 500.0, 700.0),
2959 atlas_uv: None,
2960 outline: Vec::new(),
2961 });
2962 assert!(font.glyph('A').is_none()); assert!(font.glyph('A').is_some());
2965 assert_eq!(font.glyph('A').unwrap().advance_width, 500.0);
2966 }
2967
2968 #[test]
2969 fn material_param_variants() {
2970 let m = MaterialAsset::default();
2971 assert!(matches!(m.roughness, MaterialParam::Value(_)));
2972 let tex = MaterialParam::Texture(AssetPath::new("rough.png"));
2973 assert!(matches!(tex, MaterialParam::Texture(_)));
2974 }
2975}