lyra-engine/lyra-resource/src/resource_manager.rs

506 lines
18 KiB
Rust

use std::{any::Any, collections::HashMap, path::Path, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration};
use async_std::task;
use crossbeam::channel::Receiver;
use notify::{Watcher, RecommendedWatcher};
use notify_debouncer_full::{DebouncedEvent, FileIdMap};
use thiserror::Error;
use uuid::Uuid;
use crate::{gltf::ModelLoader, loader::{ImageLoader, LoaderError, ResourceLoader}, resource::ResHandle, ResourceData, ResourceState, UntypedResHandle};
/// A trait for type erased storage of a resource.
/// Implemented for [`ResHandle<T>`]
pub trait ResourceStorage: Send + Sync + Any + 'static {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
fn as_box_any(self: Box<Self>) -> Box<dyn Any + Send + Sync>;
/// Do not set a resource to watched if it is not actually watched.
/// This is used internally.
fn set_watched(&self, watched: bool);
fn version(&self) -> usize;
fn uuid(&self) -> Uuid;
fn path(&self) -> Option<String>;
fn is_watched(&self) -> bool;
fn is_loaded(&self) -> bool;
fn set_state(&self, new: ResourceState);
fn clone_untyped(&self) -> UntypedResHandle;
}
#[derive(Error, Debug)]
pub enum RequestError {
#[error("{0}")]
Loader(LoaderError),
#[error("The file extension is unsupported: '{0}'")]
UnsupportedFileExtension(String),
#[error("The mimetype is unsupported: '{0}'")]
UnsupportedMime(String),
#[error("The identifier is not found: '{0}'")]
IdentNotFound(String),
#[error("The resource was not loaded from a path so cannot be reloaded")]
NoReloadPath
}
impl From<LoaderError> for RequestError {
fn from(value: LoaderError) -> Self {
RequestError::Loader(value)
}
}
/// A struct that stores some things used for watching resources.
pub struct ResourceWatcher {
debouncer: Arc<RwLock<notify_debouncer_full::Debouncer<RecommendedWatcher, FileIdMap>>>,
events_recv: Receiver<Result<Vec<DebouncedEvent>, Vec<notify::Error>>>,
}
/// The state of the ResourceManager
pub struct ResourceManagerState {
resources: HashMap<String, Arc<dyn ResourceStorage>>,
uuid_resources: HashMap<Uuid, Arc<dyn ResourceStorage>>,
loaders: Vec<Arc<dyn ResourceLoader>>,
watchers: HashMap<String, ResourceWatcher>,
}
/// The ResourceManager
///
/// This exists since we need the manager to be `Send + Sync`.
#[derive(Clone)]
pub struct ResourceManager {
inner: Arc<RwLock<ResourceManagerState>>,
}
impl Default for ResourceManager {
fn default() -> Self {
Self {
inner: Arc::new(RwLock::new(
ResourceManagerState {
resources: HashMap::new(),
uuid_resources: HashMap::new(),
loaders: vec![ Arc::new(ImageLoader), Arc::new(ModelLoader) ],
watchers: HashMap::new(),
}
))
}
}
}
impl ResourceManager {
pub fn new() -> Self {
Self::default()
}
/// Retrieves a non-mutable guard of the manager's state.
pub fn state(&self) -> RwLockReadGuard<ResourceManagerState> {
self.inner.read().unwrap()
}
/// Retrieves a mutable guard of the manager's state.
pub fn state_mut(&self) -> RwLockWriteGuard<ResourceManagerState> {
self.inner.write().unwrap()
}
/// Registers a loader to the manager.
pub fn register_loader<L>(&self)
where
L: ResourceLoader + Default + 'static
{
let mut state = self.state_mut();
state.loaders.push(Arc::new(L::default()));
}
/// Request a resource at `path`.
///
/// When a resource for a path is requested for the first time, the resource will be loaded
/// and cached. In the future, the cached version will be returned. There is only ever one copy
/// of the resource's data in memory at a time.
///
/// Loading resources is done asynchronously on a task spawned by `async-std`. You can use the
/// handle to check if the resource is loaded.
#[inline(always)]
pub fn request<T>(&self, path: &str) -> Result<ResHandle<T>, RequestError>
where
T: ResourceData
{
self.request_raw(path)
.map(|res| res.as_typed::<T>()
.expect("mismatched asset type, cannot downcast"))
}
/// Request a resource without downcasting to a `ResHandle<T>`.
/// Whenever you're ready to downcast, you can do so like this:
/// ```nobuild
/// let arc_any = res_arc.as_arc_any();
/// let res: Arc<ResHandle<T>> = res.downcast::<ResHandle<T>>().expect("Failure to downcast resource");
/// ```
pub fn request_raw(&self, path: &str) -> Result<UntypedResHandle, RequestError> {
let mut state = self.state_mut();
match state.resources.get(&path.to_string()) {
Some(res) => {
Ok(res.clone().clone_untyped())
},
None => {
if let Some(loader) = state.loaders.iter()
.find(|l| l.does_support_file(path)) {
// Load the resource and store it
let loader = Arc::clone(loader); // stop borrowing from self
let res = loader.load(self.clone(), path);
let handle = loader.create_erased_handle();
let untyped = handle.clone_untyped();
untyped.write().path = Some(path.to_string());
task::spawn(async move {
match res.await {
Ok(data) => {
let mut d = untyped.write();
d.state = ResourceState::Ready(data);
d.condvar.1.notify_all();
}
Err(err) => {
let mut d = untyped.write();
d.state = ResourceState::Error(Arc::new(err));
}
}
});
let res: Arc<dyn ResourceStorage> = Arc::from(handle.clone());
state.resources.insert(path.to_string(), res.clone());
state.uuid_resources.insert(res.uuid(), res);
Ok(handle.clone_untyped())
} else {
Err(RequestError::UnsupportedFileExtension(path.to_string()))
}
}
}
}
/// Store a resource using its uuid.
///
/// The resource cannot be requested with [`ResourceManager::request`], it can only be
/// retrieved with [`ResourceManager::request_uuid`].
pub fn store_uuid<T: ResourceData>(&self, res: ResHandle<T>) {
let mut state = self.state_mut();
state.resources.insert(res.uuid().to_string(), Arc::new(res));
}
/// Request a resource via its uuid.
///
/// Returns `None` if the resource was not found. The resource must of had been
/// stored with [`ResourceManager::request`] to return `Some`.
pub fn request_uuid<T: ResourceData>(&self, uuid: &Uuid) -> Option<ResHandle<T>> {
let state = self.state();
match state.resources.get(&uuid.to_string())
.or_else(|| state.uuid_resources.get(&uuid))
{
Some(res) => {
let res = res.clone().as_arc_any();
let res: Arc<ResHandle<T>> = res.downcast::<ResHandle<T>>().expect("Failure to downcast resource");
Some(ResHandle::<T>::clone(&res))
},
None => None,
}
}
/// Store bytes in the manager. If there is already an entry with the same identifier it will be updated.
///
/// Panics: If there is already an entry with the same `ident`, and the entry is not bytes, this function will panic.
///
/// Parameters:
/// * `ident` - The identifier to store along with these bytes. Make sure its unique to avoid overriding something.
/// * `bytes` - The bytes to store.
///
/// Returns: The `Arc` to the now stored resource
pub fn load_bytes<T>(&self, ident: &str, mime_type: &str, bytes: Vec<u8>, offset: usize, length: usize) -> Result<ResHandle<T>, RequestError>
where
T: ResourceData
{
let mut state = self.state_mut();
if let Some(loader) = state.loaders.iter()
.find(|l| l.does_support_mime(mime_type)) {
let loader = loader.clone();
let res = loader.load_bytes(self.clone(), bytes, offset, length);
let handle = ResHandle::<T>::new_loading(None);
let thand = handle.clone();
task::spawn(async move {
match res.await {
Ok(data) => {
let mut d = thand.write();
d.state = ResourceState::Ready(data);
}
Err(err) => {
let mut d = thand.write();
d.state = ResourceState::Error(Arc::new(err));
}
}
});
let res: Arc<dyn ResourceStorage> = Arc::from(handle.clone());
state.resources.insert(ident.to_string(), res.clone());
state.uuid_resources.insert(res.uuid(), res);
Ok(handle)
} else {
Err(RequestError::UnsupportedMime(mime_type.to_string()))
}
}
/// Requests bytes from the manager.
pub fn request_loaded_bytes<T>(&self, ident: &str) -> Result<Arc<ResHandle<T>>, RequestError>
where
T: ResourceData
{
let state = self.state();
match state.resources.get(&ident.to_string()) {
Some(res) => {
let res = res.clone().as_arc_any();
let res = res.downcast::<ResHandle<T>>().expect("Failure to downcast resource");
Ok(res)
},
None => {
Err(RequestError::IdentNotFound(ident.to_string()))
}
}
}
/// Start watching a path for changes. Returns a mspc channel that will send events
pub fn watch(&self, path: &str, recursive: bool) -> notify::Result<Receiver<Result<Vec<DebouncedEvent>, Vec<notify::Error>>>> {
let (send, recv) = crossbeam::channel::bounded(15);
let mut watcher = notify_debouncer_full::new_debouncer(Duration::from_millis(1000), None, send)?;
let recurse_mode = match recursive {
true => notify::RecursiveMode::Recursive,
false => notify::RecursiveMode::NonRecursive,
};
watcher.watcher().watch(path.as_ref(), recurse_mode)?;
let watcher = Arc::new(RwLock::new(watcher));
let watcher = ResourceWatcher {
debouncer: watcher,
events_recv: recv.clone(),
};
let mut state = self.state_mut();
state.watchers.insert(path.to_string(), watcher);
let res = state.resources.get(&path.to_string())
.expect("The path that was watched has not been loaded as a resource yet");
res.set_watched(true);
Ok(recv)
}
/// Stops watching a path
pub fn stop_watching(&self, path: &str) -> notify::Result<()> {
let state = self.state();
if let Some(watcher) = state.watchers.get(path) {
let mut watcher = watcher.debouncer.write().unwrap();
watcher.watcher().unwatch(Path::new(path))?;
// unwrap is safe since only loaded resources can be watched
let res = state.resources.get(&path.to_string()).unwrap();
res.set_watched(false);
}
Ok(())
}
/// Returns a mspc receiver for watcher events of a specific path. The path must already
/// be watched with [`ResourceManager::watch`] for this to return `Some`.
pub fn watcher_event_recv(&self, path: &str) -> Option<Receiver<Result<Vec<DebouncedEvent>, Vec<notify::Error>>>> {
let state = self.state();
state.watchers.get(&path.to_string())
.map(|w| w.events_recv.clone())
}
/// Trigger a reload of a resource.
///
/// The version of the resource will be incremented by one.
///
/// > Note: Since reloading is done asynchronously, the reloaded data will not be immediately
/// accessible. Until the resource is reloaded, the previous data will stay inside of
/// the handle.
pub fn reload<T>(&self, resource: ResHandle<T>) -> Result<(), RequestError>
where
T: ResourceData
{
let state = self.state();
let path = resource.path()
.ok_or(RequestError::NoReloadPath)?;
if let Some(loader) = state.loaders.iter()
.find(|l| l.does_support_file(&path)) {
let loader = Arc::clone(loader); // stop borrowing from self
let res = loader.load(self.clone(), &path);
/* let res_lock = &resource.data;
let mut res_lock = res_lock.write().unwrap();
res_lock.state = ResourceState::Loading;
drop(res_lock); */
let thand = resource.clone();
task::spawn(async move {
match res.await {
Ok(data) => {
let mut d = thand.write();
d.state = ResourceState::Ready(data);
d.version += 1;
}
Err(err) => {
let mut d = thand.write();
d.state = ResourceState::Error(Arc::new(err));
}
}
});
}
Ok(())
}
}
#[cfg(test)]
pub(crate) mod tests {
use std::{io, ops::Deref};
use instant::Instant;
use crate::{Image, ResourceData};
use super::*;
fn get_image(path: &str) -> String {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
format!("{manifest}/test_files/img/{path}")
}
pub(crate) fn busy_wait_resource<R: ResourceData>(handle: &ResHandle<R>, timeout: f32) {
let start = Instant::now();
while !handle.is_loaded() {
// loop until the image is loaded
let now = Instant::now();
let diff = now - start;
if diff.as_secs_f32() >= timeout {
panic!("Image never loaded");
}
}
}
pub(crate) fn busy_wait_resource_reload<R: ResourceData>(handle: &ResHandle<R>, timeout: f32) {
let version = handle.version();
let start = Instant::now();
while !handle.is_loaded() || handle.version() == version {
// loop until the image is loaded
let now = Instant::now();
let diff = now - start;
if diff.as_secs_f32() >= timeout {
panic!("Image never loaded");
}
}
}
#[test]
fn load_image() {
let man = ResourceManager::new();
let res = man.request::<Image>(&get_image("squiggles.png")).unwrap();
assert!(!res.is_loaded());
res.wait_for_load();
//busy_wait_resource(&res, 10.0);
// shouldn't panic because of the loop
res.data_ref().unwrap();
}
/// Ensures that only one copy of the same data was made
#[test]
fn ensure_single() {
let man = ResourceManager::new();
let res = man.request::<Image>(&get_image("squiggles.png")).unwrap();
assert_eq!(Arc::strong_count(&res.handle.res), 3);
let resagain = man.request::<Image>(&get_image("squiggles.png")).unwrap();
assert_eq!(Arc::strong_count(&resagain.handle.res), 4);
}
/// Ensures that an error is returned when a file that doesn't exist is requested
#[test]
fn ensure_none() {
let man = ResourceManager::new();
let res = man.request::<Image>(&get_image("squigglesfff.png")).unwrap();
//let err = res.err().unwrap();
// 1 second should be enough to run into an error
std::thread::sleep(Duration::from_secs(1));
//busy_wait_resource(&res, 10.0);
let state = &res.read().state;
assert!(
match state {
// make sure the error is NotFound
//RequestError::Loader(LoaderError::IoError(e)) if e.kind() == io::ErrorKind::NotFound => true,
ResourceState::Error(err) => {
match err.deref() {
LoaderError::IoError(e) if e.kind() == io::ErrorKind::NotFound => true,
_ => false,
}
},
_ => false
}
);
}
#[test]
fn reload_image() {
let man = ResourceManager::new();
let res = man.request::<Image>(&get_image("squiggles.png")).unwrap();
busy_wait_resource(&res, 10.0);
let img = res.data_ref();
img.unwrap();
man.reload(res.clone()).unwrap();
busy_wait_resource_reload(&res, 10.0);
assert_eq!(res.version(), 1);
man.reload(res.clone()).unwrap();
busy_wait_resource_reload(&res, 10.0);
assert_eq!(res.version(), 2);
}
#[test]
fn watch_image() {
let orig_path = get_image("squiggles.png");
let image_path = get_image("squiggles_test.png");
std::fs::copy(orig_path, &image_path).unwrap();
let man = ResourceManager::new();
let res = man.request::<Image>(&image_path).unwrap();
busy_wait_resource(&res, 10.0);
let img = res.data_ref();
img.unwrap();
let recv = man.watch(&image_path, false).unwrap();
std::fs::remove_file(&image_path).unwrap();
let event = recv.recv().unwrap();
let event = event.unwrap();
assert!(event.iter().any(|ev| ev.kind.is_remove() || ev.kind.is_modify()));
}
}