blade_asset/
lib.rs

1#![allow(clippy::new_without_default)]
2#![warn(
3    trivial_casts,
4    trivial_numeric_casts,
5    unused_extern_crates,
6    unused_qualifications,
7    // We don't match on a reference, unless required.
8    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
31/// Handle representing an asset.
32pub 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    // Boxed erased type of metadata
78    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
117/// A container for storing the result of cooking.
118///
119/// It's meant to live only temporarily during an asset loading.
120/// It receives the result of cooking and then delivers it to
121/// a task that writes the data to disk.
122///
123/// Here `T` is the cooked asset type.
124pub struct Cooker<B> {
125    inner: Mutex<Inner>,
126    base_path: PathBuf,
127    _phantom: PhantomData<B>,
128}
129// T doesn't matter for Send/Sync, since we aren't storing it here.
130unsafe impl<B> Send for Cooker<B> {}
131unsafe impl<B> Sync for Cooker<B> {}
132
133impl<B: Baker> Cooker<B> {
134    /// Create a new container with no data.
135    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    /// Create a new container with no data, no path, and no hasher.
148    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    /// Return the base path of the asset.
163    pub fn base_path(&self) -> &Path {
164        &self.base_path
165    }
166
167    /// Put the data into it.
168    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    /// Read another file as a dependency.
175    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                // Read the file at the same time as we include the hash
182                // of its modification time in the header.
183                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
197/// Baker class abstracts over asset-specific logic.
198pub trait Baker: Sized + Send + Sync + 'static {
199    /// Metadata used for loading assets.
200    type Meta: Clone + Eq + Hash + Send + fmt::Display;
201    /// Intermediate data that is cached, which comes out as a result of cooking.
202    type Data<'a>: Flat;
203    /// Output type that is produced for the client.
204    type Output: Send;
205    /// Cook an asset represented by a slice of bytes.
206    ///
207    /// This method is called within a task within the `exe_context` execution context.
208    /// It may fork out other tasks if necessary.
209    /// It must put the result into `result` at some point during execution.
210    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    /// Produce the output bsed on a cooked asset.
219    ///
220    /// This method is also called within a task `exe_context`.
221    fn serve(&self, cooked: Self::Data<'_>, exe_context: &choir::ExecutionContext) -> Self::Output;
222    /// Delete the output of an asset.
223    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)] // bad warning?
245fn 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
295/// Manager of assets.
296///
297/// Contains common logic for tracking the `Handle` associations,
298/// caching the results of cooking by the path,
299/// and scheduling tasks for cooking and serving assets.
300pub 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    /// Asset-specific implementation.
307    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    /// Create a new asset manager.
321    ///
322    /// The `target` points to the folder to store cooked assets in.
323    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        // The name hash includes the parent path and the metadata.
344        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(); // write zero hash first
401                    file.write_all(&[0; 8]).unwrap(); // write zero data offset
402                                                      // write down the dependencies
403                    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                    // Write the real hash last, so that the cached file is not valid
413                    // unless everything went smooth.
414                    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            // Note: this task is separate, because it may spawn sub-tasks.
433            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                    // Read the source file through the same mechanism as the
440                    // dependencies, so that its modified time makes it into the hash.
441                    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    /// Load an asset given the data directly.
504    ///
505    /// The `name` must be a pretend file name, with a proper extension.
506    ///
507    /// Doesn't get cached by the manager, always goes through the cooking process.
508    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    /// Load an asset given the relative path.
533    ///
534    /// Metadata is an asset-specific piece of information that determines how the asset is processed.
535    /// Each pair of (path, meta) is cached indepedently.
536    ///
537    /// This function produces a handle for the asset, and also returns the load task.
538    /// It's only valid to access the asset once the load task is completed.
539    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    /// Load an asset that has been pre-cooked already.
558    ///
559    /// Expected to run inside a task.
560    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    /// Clear the asset manager by deleting all the stored assets.
581    ///
582    /// Invalidates all handles produced from loading assets.
583    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    /// Hot reload a changed asset.
601    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}