1#![allow(clippy::new_without_default)]
2#![warn(
3 trivial_casts,
4 trivial_numeric_casts,
5 unused_extern_crates,
6 unused_qualifications,
7 clippy::pattern_type_mismatch,
9)]
10
11use std::{
12 any::TypeId,
13 collections::hash_map::{DefaultHasher, Entry, HashMap},
14 fmt, fs,
15 hash::{Hash, Hasher},
16 io::{Read, Seek as _, SeekFrom},
17 marker::PhantomData,
18 mem, ops,
19 path::{Path, PathBuf},
20 ptr, str,
21 sync::{Arc, Mutex},
22};
23
24mod arena;
25mod flat;
26
27pub use flat::{round_up, Flat};
28
29type Version = u32;
30
31pub struct Handle<T> {
33 inner: arena::Handle<Slot<T>>,
34 version: Version,
35}
36impl<T> Clone for Handle<T> {
37 fn clone(&self) -> Self {
38 Handle {
39 inner: self.inner,
40 version: self.version,
41 }
42 }
43}
44impl<T> Copy for Handle<T> {}
45impl<T> PartialEq for Handle<T> {
46 fn eq(&self, other: &Self) -> bool {
47 self.inner == other.inner && self.version == other.version
48 }
49}
50impl<T> Eq for Handle<T> {}
51impl<T> Hash for Handle<T> {
52 fn hash<H: Hasher>(&self, hasher: &mut H) {
53 self.inner.hash(hasher);
54 }
55}
56impl<T> fmt::Debug for Handle<T> {
57 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58 f.debug_struct("Handle")
59 .field("inner", &self.inner)
60 .field("version", &self.version)
61 .finish()
62 }
63}
64
65struct DataRef<T> {
66 data: *mut Option<T>,
67 version: *mut Version,
68 sources: *mut Vec<PathBuf>,
69}
70unsafe impl<T> Send for DataRef<T> {}
71
72struct Slot<T> {
73 load_task: Option<choir::RunningTask>,
74 version: Version,
75 base_path: PathBuf,
76 sources: Vec<PathBuf>,
77 meta: *const (),
79 data: Option<T>,
80}
81unsafe impl<T> Send for Slot<T> {}
82unsafe impl<T> Sync for Slot<T> {}
83
84impl<T> Default for Slot<T> {
85 fn default() -> Self {
86 Self {
87 load_task: None,
88 version: 0,
89 base_path: PathBuf::default(),
90 sources: Vec::new(),
91 meta: ptr::null(),
92 data: None,
93 }
94 }
95}
96
97#[derive(Default)]
98struct Inner {
99 result: Vec<u8>,
100 dependencies: Vec<PathBuf>,
101 hasher: DefaultHasher,
102}
103
104#[allow(unused)]
105struct CachedSourceDependency {
106 relative_path_length: usize,
107 relative_path: *const u8,
108}
109#[allow(unused)]
110struct CachedAssetHeader {
111 hash: u64,
112 data_offset: u64,
113 source_dependency_count: usize,
114 source_dependencies: *const CachedSourceDependency,
115}
116
117pub struct Cooker<B> {
125 inner: Mutex<Inner>,
126 base_path: PathBuf,
127 _phantom: PhantomData<B>,
128}
129unsafe impl<B> Send for Cooker<B> {}
131unsafe impl<B> Sync for Cooker<B> {}
132
133impl<B: Baker> Cooker<B> {
134 pub fn new(base_path: &Path, hasher: DefaultHasher) -> Self {
136 Self {
137 inner: Mutex::new(Inner {
138 result: Vec::new(),
139 dependencies: Vec::new(),
140 hasher,
141 }),
142 base_path: base_path.to_path_buf(),
143 _phantom: PhantomData,
144 }
145 }
146
147 pub fn new_embedded() -> Self {
149 Self {
150 inner: Mutex::new(Inner::default()),
151 base_path: Default::default(),
152 _phantom: PhantomData,
153 }
154 }
155
156 pub fn extract_embedded(&self) -> Vec<u8> {
157 let mut inner = self.inner.lock().unwrap();
158 assert!(inner.dependencies.is_empty());
159 mem::take(&mut inner.result)
160 }
161
162 pub fn base_path(&self) -> &Path {
164 &self.base_path
165 }
166
167 pub fn finish(&self, value: B::Data<'_>) {
169 let mut inner = self.inner.lock().unwrap();
170 inner.result = vec![0u8; value.size()];
171 unsafe { value.write(inner.result.as_mut_ptr()) };
172 }
173
174 pub fn add_dependency(&self, relative_path: &Path) -> Vec<u8> {
176 let mut inner = self.inner.lock().unwrap();
177 inner.dependencies.push(relative_path.to_path_buf());
178 let full_path = self.base_path.join(relative_path);
179 match fs::File::open(&full_path) {
180 Ok(mut file) => {
181 let mut buf = Vec::new();
184 file.metadata()
185 .unwrap()
186 .modified()
187 .unwrap()
188 .hash(&mut inner.hasher);
189 file.read_to_end(&mut buf).unwrap();
190 buf
191 }
192 Err(e) => panic!("Unable to read {}: {:?}", full_path.display(), e),
193 }
194 }
195}
196
197pub trait Baker: Sized + Send + Sync + 'static {
199 type Meta: Clone + Eq + Hash + Send + fmt::Display;
201 type Data<'a>: Flat;
203 type Output: Send;
205 fn cook(
211 &self,
212 source: &[u8],
213 extension: &str,
214 meta: Self::Meta,
215 cooker: Arc<Cooker<Self>>,
216 exe_context: &choir::ExecutionContext,
217 );
218 fn serve(&self, cooked: Self::Data<'_>, exe_context: &choir::ExecutionContext) -> Self::Output;
222 fn delete(&self, output: Self::Output);
224}
225
226#[derive(Debug)]
227enum InvalidDependency {
228 MalformedPath,
229 DoesntExist,
230 NotFile,
231}
232
233#[derive(Debug)]
234enum CookReason {
235 NoTarget,
236 BadHeader,
237 TooManyDependencies(usize),
238 Dependency(usize, InvalidDependency),
239 Outdated,
240 WrongDataOffset,
241}
242
243#[profiling::function]
244#[allow(clippy::read_zero_byte_vec)] fn check_target_relevancy(
246 target_path: &Path,
247 base_path: &Path,
248 mut hasher: DefaultHasher,
249) -> Result<(), CookReason> {
250 let mut file = fs::File::open(target_path).map_err(|_| CookReason::NoTarget)?;
251 let mut hash_bytes = [0u8; 8];
252 file.read_exact(&mut hash_bytes)
253 .map_err(|_| CookReason::BadHeader)?;
254 let current_hash = u64::from_le_bytes(hash_bytes);
255 file.read_exact(&mut hash_bytes)
256 .map_err(|_| CookReason::BadHeader)?;
257 let data_offset = u64::from_le_bytes(hash_bytes);
258
259 let mut temp_bytes = [0u8; mem::size_of::<usize>()];
260 file.read_exact(&mut temp_bytes)
261 .map_err(|_| CookReason::BadHeader)?;
262 let num_deps = usize::from_le_bytes(temp_bytes);
263 if num_deps > 100 {
264 return Err(CookReason::TooManyDependencies(num_deps));
265 }
266 let mut dep_str = Vec::new();
267 for i in 0..num_deps {
268 file.read_exact(&mut temp_bytes)
269 .map_err(|_| CookReason::BadHeader)?;
270 let str_len = usize::from_le_bytes(temp_bytes);
271 dep_str.resize(str_len, 0u8);
272 file.read_exact(&mut dep_str)
273 .map_err(|_| CookReason::BadHeader)?;
274 let dep_path = base_path.join(
275 str::from_utf8(&dep_str)
276 .map_err(|_| CookReason::Dependency(i, InvalidDependency::MalformedPath))?,
277 );
278 let metadata = fs::metadata(dep_path)
279 .map_err(|_| CookReason::Dependency(i, InvalidDependency::DoesntExist))?;
280 if !metadata.is_file() {
281 return Err(CookReason::Dependency(i, InvalidDependency::NotFile));
282 }
283 metadata.modified().unwrap().hash(&mut hasher);
284 }
285
286 if hasher.finish() != current_hash {
287 Err(CookReason::Outdated)
288 } else if file.stream_position().unwrap() != data_offset {
289 Err(CookReason::WrongDataOffset)
290 } else {
291 Ok(())
292 }
293}
294
295pub struct AssetManager<B: Baker> {
301 target: PathBuf,
302 slots: arena::Arena<Slot<B::Output>>,
303 #[allow(clippy::type_complexity)]
304 paths: Mutex<HashMap<(PathBuf, B::Meta), Handle<B::Output>>>,
305 pub choir: Arc<choir::Choir>,
306 pub baker: Arc<B>,
308}
309
310impl<B: Baker> ops::Index<Handle<B::Output>> for AssetManager<B> {
311 type Output = B::Output;
312 fn index(&self, handle: Handle<B::Output>) -> &Self::Output {
313 let slot = &self.slots[handle.inner];
314 assert_eq!(handle.version, slot.version, "Outdated {:?}", handle);
315 slot.data.as_ref().unwrap()
316 }
317}
318
319impl<B: Baker> AssetManager<B> {
320 pub fn new(target: &Path, choir: &Arc<choir::Choir>, baker: B) -> Self {
324 if !target.is_dir() {
325 log::info!("Creating target {}", target.display());
326 fs::create_dir_all(target).unwrap();
327 }
328 Self {
329 target: target.to_path_buf(),
330 slots: arena::Arena::new(64),
331 paths: Mutex::default(),
332 choir: Arc::clone(choir),
333 baker: Arc::new(baker),
334 }
335 }
336
337 pub fn get_main_source_path(&self, handle: Handle<B::Output>) -> Option<&PathBuf> {
338 self.slots[handle.inner].sources.first()
339 }
340
341 fn make_target_path(&self, base_path: &Path, file_name: &Path, meta: &B::Meta) -> PathBuf {
342 use base64::engine::{general_purpose::URL_SAFE as ENCODING_ENGINE, Engine as _};
343 let mut hasher = DefaultHasher::new();
345 base_path.hash(&mut hasher);
346 meta.hash(&mut hasher);
347 let hash = hasher.finish().to_le_bytes();
348 let mut file_name_str = format!("{}-", file_name.display());
349 ENCODING_ENGINE.encode_string(hash, &mut file_name_str);
350 file_name_str += ".raw";
351 self.target.join(file_name_str)
352 }
353
354 fn create_impl<'a>(
355 &self,
356 slot: &'a mut Slot<B::Output>,
357 file_name: &Path,
358 content: Option<&[u8]>,
359 ) -> Option<(u32, &'a choir::RunningTask)> {
360 use std::{hash::Hasher as _, io::Write as _};
361
362 let version = slot.version + 1;
363 let (task_option, meta, data_ref) = (
364 &mut slot.load_task,
365 unsafe { &*(slot.meta as *const B::Meta) },
366 DataRef {
367 data: &mut slot.data,
368 version: &mut slot.version,
369 sources: &mut slot.sources,
370 },
371 );
372
373 let target_path = self.make_target_path(&slot.base_path, file_name, meta);
374 let file_name = file_name.to_owned();
375 let content = content.map(Vec::from);
376 let mut hasher = DefaultHasher::new();
377 TypeId::of::<B::Data<'static>>().hash(&mut hasher);
378
379 let load_task = if let Err(reason) =
380 check_target_relevancy(&target_path, &slot.base_path, hasher.clone())
381 {
382 log::info!(
383 "Cooking {:?}: {} version={}",
384 reason,
385 file_name.display(),
386 version
387 );
388 let cooker = Arc::new(Cooker::new(&slot.base_path, hasher));
389 let cooker_arg = Arc::clone(&cooker);
390 let baker = Arc::clone(&self.baker);
391 let mut load_task = self
392 .choir
393 .spawn(format!("cook finish for {}", file_name.display()))
394 .init(move |exe_context| {
395 let mut inner = cooker.inner.lock().unwrap();
396 assert!(!inner.result.is_empty());
397 let mut file = fs::File::create(&target_path).unwrap_or_else(|e| {
398 panic!("Unable to create {}: {}", target_path.display(), e)
399 });
400 file.write_all(&[0; 8]).unwrap(); file.write_all(&[0; 8]).unwrap(); file.write_all(&inner.dependencies.len().to_le_bytes())
404 .unwrap();
405 for dep in inner.dependencies.iter() {
406 let dep_bytes = dep.to_str().unwrap().as_bytes();
407 file.write_all(&dep_bytes.len().to_le_bytes()).unwrap();
408 file.write_all(dep_bytes).unwrap();
409 }
410 let data_offset = file.stream_position().unwrap();
411 file.write_all(&inner.result).unwrap();
412 file.seek(SeekFrom::Start(0)).unwrap();
415 let hash = inner.hasher.finish();
416 file.write_all(&hash.to_le_bytes()).unwrap();
417 file.write_all(&data_offset.to_le_bytes()).unwrap();
418
419 let dr = data_ref;
420 if let Some(data) = unsafe { (*dr.data).take() } {
421 baker.delete(data);
422 }
423 let cooked = unsafe { <B::Data<'_> as Flat>::read(inner.result.as_ptr()) };
424 let target = baker.serve(cooked, &exe_context);
425 unsafe {
426 *dr.data = Some(target);
427 *dr.version = version;
428 *dr.sources = mem::take(&mut inner.dependencies);
429 }
430 });
431
432 let baker = Arc::clone(&self.baker);
434 let meta = meta.clone();
435 let cook_task = self
436 .choir
437 .spawn(format!("cook {} as {}", file_name.display(), meta))
438 .init(move |exe_context| {
439 let extension = file_name.extension().unwrap().to_str().unwrap();
442 let source = match content {
443 Some(data) => data,
444 None => cooker_arg.add_dependency(&file_name),
445 };
446 baker.cook(&source, extension, meta, cooker_arg, &exe_context);
447 });
448
449 load_task.depend_on(&cook_task);
450 load_task
451 } else if task_option.is_none() {
452 let baker = Arc::clone(&self.baker);
453 self.choir
454 .spawn(format!("load {} with {}", file_name.display(), meta))
455 .init(move |exe_context| {
456 let mut file = fs::File::open(target_path).unwrap();
457 let mut bytes = [0u8; 8];
458 file.read_exact(&mut bytes).unwrap();
459 let _hash = u64::from_le_bytes(bytes);
460 file.read_exact(&mut bytes).unwrap();
461 let offset = u64::from_le_bytes(bytes);
462 file.seek(SeekFrom::Start(offset)).unwrap();
463 let mut data = Vec::new();
464 file.read_to_end(&mut data).unwrap();
465 let cooked = unsafe { <B::Data<'_> as Flat>::read(data.as_ptr()) };
466 let target = baker.serve(cooked, &exe_context);
467 let dr = data_ref;
468 unsafe {
469 *dr.data = Some(target);
470 *dr.version = version;
471 (*dr.sources).push(file_name);
472 }
473 })
474 } else {
475 return None;
476 };
477
478 let running_task = task_option.insert(load_task.run());
479 Some((version, running_task))
480 }
481
482 fn create(&self, source_path: &Path, meta: B::Meta) -> Handle<B::Output> {
483 let (handle, slot_ptr) = self.slots.alloc_default();
484 let slot = unsafe { &mut *slot_ptr };
485 assert_eq!(slot.version, 0);
486 *slot = Slot {
487 base_path: source_path
488 .parent()
489 .unwrap_or_else(|| Path::new("."))
490 .to_owned(),
491 meta: Box::into_raw(Box::new(meta)) as *const _,
492 ..Default::default()
493 };
494
495 let file_name = Path::new(source_path.file_name().unwrap());
496 let (version, _) = self.create_impl(slot, file_name, None).unwrap();
497 Handle {
498 inner: handle,
499 version,
500 }
501 }
502
503 pub fn load_data(
509 &self,
510 name: &Path,
511 data: &[u8],
512 meta: B::Meta,
513 ) -> (Handle<B::Output>, &choir::RunningTask) {
514 let (handle, slot_ptr) = self.slots.alloc_default();
515 let slot = unsafe { &mut *slot_ptr };
516 assert_eq!(slot.version, 0);
517 *slot = Slot {
518 meta: Box::into_raw(Box::new(meta)) as *const _,
519 ..Default::default()
520 };
521
522 let (version, _) = self.create_impl(slot, name, Some(data)).unwrap();
523
524 let task = self.slots[handle].load_task.as_ref().unwrap();
525 let out_handle = Handle {
526 inner: handle,
527 version,
528 };
529 (out_handle, task)
530 }
531
532 pub fn load(
540 &self,
541 path: impl AsRef<Path>,
542 meta: B::Meta,
543 ) -> (Handle<B::Output>, &choir::RunningTask) {
544 let path_buf = path.as_ref().to_path_buf();
545 let mut paths = self.paths.lock().unwrap();
546 let handle = match paths.entry((path_buf, meta)) {
547 Entry::Occupied(e) => *e.get(),
548 Entry::Vacant(e) => {
549 let handle = self.create(&e.key().0, e.key().1.clone());
550 *e.insert(handle)
551 }
552 };
553 let task = self.slots[handle.inner].load_task.as_ref().unwrap();
554 (handle, task)
555 }
556
557 pub fn load_cooked_inside_task(
561 &self,
562 cooked: B::Data<'_>,
563 exe_context: &choir::ExecutionContext,
564 ) -> Handle<B::Output> {
565 let value = self.baker.serve(cooked, exe_context);
566 let (handle, slot_ptr) = self.slots.alloc_default();
567 let slot = unsafe { &mut *slot_ptr };
568 assert_eq!(slot.version, 0);
569 *slot = Slot {
570 version: 1,
571 data: Some(value),
572 ..Slot::default()
573 };
574 Handle {
575 inner: handle,
576 version: slot.version,
577 }
578 }
579
580 pub fn clear(&self) {
584 self.paths.lock().unwrap().clear();
585 self.slots.dealloc_each(|_handle, slot| {
586 if let Some(task) = slot.load_task {
587 task.join();
588 }
589 if let Some(data) = slot.data {
590 self.baker.delete(data);
591 }
592 if !slot.meta.is_null() {
593 unsafe {
594 let _ = Box::from_raw(slot.meta as *mut B::Meta);
595 }
596 }
597 })
598 }
599
600 pub fn hot_reload(&self, handle: &mut Handle<B::Output>) -> Option<&choir::RunningTask> {
602 let slot = unsafe { &mut *self.slots.get_mut_ptr(handle.inner) };
603 let file_name = slot.sources.first().unwrap().to_owned();
604 self.create_impl(slot, &file_name, None)
605 .map(|(version, task)| {
606 handle.version = version;
607 task
608 })
609 }
610
611 pub fn list_running_tasks(&self, list: &mut Vec<choir::RunningTask>) {
612 self.slots.for_each(|_, slot| {
613 if let Some(ref task) = slot.load_task {
614 if !task.is_done() {
615 list.push(task.clone());
616 }
617 }
618 });
619 }
620}