diff --git a/lyra-resource/Cargo.toml b/lyra-resource/Cargo.toml index bbc45e8..ffdef11 100644 --- a/lyra-resource/Cargo.toml +++ b/lyra-resource/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] anyhow = "1.0.75" base64 = "0.21.4" +crossbeam = { version = "0.8.4", features = [ "crossbeam-channel" ] } edict = "0.5.0" glam = "0.24.1" gltf = { version = "1.3.0", features = ["KHR_materials_pbrSpecularGlossiness", "KHR_materials_specular"] } @@ -15,6 +16,8 @@ image = "0.24.7" # not using custom matcher, or file type from file path infer = { version = "0.15.0", default-features = false } mime = "0.3.17" +notify = "6.1.1" +#notify = { version = "6.1.1", default-features = false, features = [ "fsevent-sys", "macos_fsevent" ]} # disables crossbeam-channel percent-encoding = "2.3.0" thiserror = "1.0.48" tracing = "0.1.37" diff --git a/lyra-resource/src/loader/image.rs b/lyra-resource/src/loader/image.rs index 8a1940b..d7c3df7 100644 --- a/lyra-resource/src/loader/image.rs +++ b/lyra-resource/src/loader/image.rs @@ -2,7 +2,7 @@ use std::{fs::File, sync::Arc, io::Read}; use image::ImageError; -use crate::{resource_manager::ResourceStorage, texture::Texture, resource::Resource, ResourceManager}; +use crate::{resource_manager::ResourceStorage, texture::Texture, resource::ResHandle, ResourceManager}; use super::{LoaderError, ResourceLoader}; @@ -62,7 +62,7 @@ impl ResourceLoader for ImageLoader { let texture = Texture { image, }; - let res = Resource::with_data(path, texture); + let res = ResHandle::with_data(path, texture); Ok(Arc::new(res)) } @@ -76,7 +76,7 @@ impl ResourceLoader for ImageLoader { let texture = Texture { image, }; - let res = Resource::with_data(&uuid::Uuid::new_v4().to_string(), texture); + let res = ResHandle::with_data(&uuid::Uuid::new_v4().to_string(), texture); Ok(Arc::new(res)) } diff --git a/lyra-resource/src/loader/model.rs b/lyra-resource/src/loader/model.rs index c9caf4b..396eae5 100644 --- a/lyra-resource/src/loader/model.rs +++ b/lyra-resource/src/loader/model.rs @@ -2,7 +2,7 @@ use std::{sync::Arc, path::PathBuf}; use thiserror::Error; -use crate::{ResourceLoader, LoaderError, Mesh, Model, MeshVertexAttribute, VertexAttributeData, Resource, Material, MeshIndices, ResourceManager, util}; +use crate::{ResourceLoader, LoaderError, Mesh, Model, MeshVertexAttribute, VertexAttributeData, ResHandle, Material, MeshIndices, ResourceManager, util}; use tracing::debug; @@ -189,7 +189,7 @@ impl ResourceLoader for ModelLoader { .collect(); debug!("Loaded {} meshes, and {} materials from '{}'", meshes.len(), materials.len(), path); - Ok(Arc::new(Resource::with_data(path, Model::new(meshes)))) + Ok(Arc::new(ResHandle::with_data(path, Model::new(meshes)))) } #[allow(unused_variables)] @@ -216,8 +216,8 @@ mod tests { let mut manager = ResourceManager::new(); let loader = ModelLoader::default(); let model = loader.load(&mut manager, &path).unwrap(); - let model = Arc::downcast::>(model.as_arc_any()).unwrap(); - let model = model.data.as_ref().unwrap(); + let model = Arc::downcast::>(model.as_arc_any()).unwrap(); + let model = model.data_ref(); assert_eq!(model.meshes.len(), 1); // There should only be 1 mesh let mesh = &model.meshes[0]; assert!(mesh.position().unwrap().len() > 0); diff --git a/lyra-resource/src/resource.rs b/lyra-resource/src/resource.rs index 2dbb2a6..d36c82f 100644 --- a/lyra-resource/src/resource.rs +++ b/lyra-resource/src/resource.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use uuid::Uuid; @@ -8,25 +8,140 @@ pub enum ResourceState { Ready, } -#[derive(Clone)] -pub struct Resource { - pub path: String, - pub data: Option>, - pub uuid: Uuid, - pub state: ResourceState, +pub struct ResourceDataRef<'a, T> { + guard: std::sync::RwLockReadGuard<'a, Resource>, } -/// A helper type to make it easier to use resources -pub type ResHandle = Arc>; +impl<'a, T> std::ops::Deref for ResourceDataRef<'a, T> { + type Target = T; -impl Resource { + fn deref(&self) -> &Self::Target { + // safety: this struct must only be created if the resource is loaded + self.guard.data.as_ref().unwrap() + } +} + +pub(crate) struct Resource { + path: String, + pub(crate) data: Option, + pub(crate) version: usize, + pub(crate) state: ResourceState, + uuid: Uuid, +} + +/// A handle to a resource. +/// +/// # Note +/// This struct has an inner [`RwLock`] to the resource data, so most methods may be blocking. +/// However, the only times it will be blocking is if another thread is reloading the resource +/// and has a write lock on the data. This means that most of the time, it is not blocking. +pub struct ResHandle { + pub(crate) data: Arc>>, +} + +impl Clone for ResHandle { + fn clone(&self) -> Self { + Self { data: self.data.clone() } + } +} + +impl ResHandle { /// Create the resource with data, its assumed the state is `Ready` pub fn with_data(path: &str, data: T) -> Self { - Self { + let res_version = Resource { path: path.to_string(), - data: Some(Arc::new(data)), - uuid: Uuid::new_v4(), + data: Some(data), + version: 0, state: ResourceState::Ready, + uuid: Uuid::new_v4(), + }; + + Self { + data: Arc::new(RwLock::new(res_version)), } } -} \ No newline at end of file + + /// Returns a boolean indicated if this resource is loaded + pub fn is_loaded(&self) -> bool { + let d = self.data.read().expect("Resource mutex was poisoned!"); + d.state == ResourceState::Ready + } + + /// Returns the current state of the resource. + pub fn state(&self) -> ResourceState { + let d = self.data.read().expect("Resource mutex was poisoned!"); + d.state + } + + /// Returns the path that the resource was loaded from. + pub fn path(&self) -> String { + let d = self.data.read().expect("Resource mutex was poisoned!"); + d.path.to_string() + } + + /// Returns the uuid of the resource. + pub fn uuid(&self) -> Uuid { + let d = self.data.read().expect("Resource mutex was poisoned!"); + d.uuid + } + + /// Retrieves the current version of the resource. This gets incremented when the resource + /// is reloaded. + pub fn version(&self) -> usize { + let d = self.data.read().expect("Resource mutex was poisoned!"); + d.version + } + + /// Get a reference to the data in the resource + /// + /// # Panics + /// Panics if the resource was not loaded yet. + pub fn data_ref<'a>(&'a self) -> ResourceDataRef<'a, T> { + let d = self.data.read().expect("Resource mutex was poisoned!"); + ResourceDataRef { + guard: d + } + } + + /// Attempt to get a borrow to the resource data. Returns `None` if the resource is not loaded. + pub fn try_data_ref<'a>(&'a self) -> Option> { + if self.is_loaded() { + let d = self.data.read().expect("Resource mutex was poisoned!"); + Some(ResourceDataRef { + guard: d + }) + } else { + None + } + } + + /* /// Get a reference to the data in the resource + /// + /// # Panics + /// Panics if the resource was not loaded yet. + pub fn data_ref(&self) -> &T { + self.data.as_ref() + .expect("Resource is not loaded yet (use try_data_ref, or wait until its loaded)!") + } + + /// If the resource is loaded, returns `Some` reference to the data in the resource, + /// else it will return `None` + pub fn try_data_ref(&self) -> Option<&T> { + self.data.as_ref() + } + + /// Get a **mutable** reference to the data in the resource + /// + /// # Panics + /// Panics if the resource was not loaded yet. + pub fn data_mut(&mut self) -> &mut T { + self.data.as_mut() + .expect("Resource is not loaded yet (use try_data_ref, or wait until its loaded)!") + } + + /// If the resource is loaded, returns `Some` **mutable** reference to the data in the resource, + /// else it will return `None` + pub fn try_data_mut(&mut self) -> Option<&mut T> { + self.data.as_mut() + } */ +} diff --git a/lyra-resource/src/resource_manager.rs b/lyra-resource/src/resource_manager.rs index 0224269..593c0d4 100644 --- a/lyra-resource/src/resource_manager.rs +++ b/lyra-resource/src/resource_manager.rs @@ -1,13 +1,16 @@ -use std::{sync::Arc, collections::HashMap, any::Any}; +use std::{sync::{Arc, RwLock}, collections::{HashMap, VecDeque}, any::Any, thread::{Thread, JoinHandle}, rc::Rc, path::Path, ops::Deref}; +use crossbeam::channel::{Receiver, Sender}; +use notify::{Watcher, RecommendedWatcher}; use thiserror::Error; -use crate::{resource::Resource, loader::{ResourceLoader, LoaderError, image::ImageLoader, model::ModelLoader}}; +use crate::{resource::ResHandle, loader::{ResourceLoader, LoaderError, image::ImageLoader, model::ModelLoader}}; 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) -> Arc; + fn as_box_any(self: Box) -> Box; } /// Implements this trait for anything that fits the type bounds @@ -23,6 +26,10 @@ impl ResourceStorage for T { fn as_arc_any(self: Arc) -> Arc { self.clone() } + + fn as_box_any(self: Box) -> Box { + self + } } #[derive(Error, Debug)] @@ -46,9 +53,16 @@ impl From for RequestError { /// A struct that stores all Manager data. This is requried for sending //struct ManagerStorage +/// A struct that +pub struct ResourceWatcher { + watcher: Arc>, + events_recv: Receiver>, +} + pub struct ResourceManager { resources: HashMap>, loaders: Vec>, + watchers: HashMap, } impl Default for ResourceManager { @@ -62,6 +76,7 @@ impl ResourceManager { Self { resources: HashMap::new(), loaders: vec![ Arc::new(ImageLoader), Arc::new(ModelLoader) ], + watchers: HashMap::new(), } } @@ -73,11 +88,15 @@ impl ResourceManager { self.loaders.push(Arc::new(L::default())); } - pub fn request(&mut self, path: &str) -> Result>, RequestError> { + pub fn request(&mut self, path: &str) -> Result, RequestError> + where + T: Send + Sync + Any + 'static + { match self.resources.get(&path.to_string()) { Some(res) => { let res = res.clone().as_arc_any(); - let res = res.downcast::>().expect("Failure to downcast resource"); + let res: Arc> = res.downcast::>().expect("Failure to downcast resource"); + let res = ResHandle::::clone(&res); Ok(res) }, @@ -88,12 +107,14 @@ impl ResourceManager { // Load the resource and store it let loader = Arc::clone(loader); // stop borrowing from self let res = loader.load(self, path)?; + let res: Arc = Arc::from(res); self.resources.insert(path.to_string(), res.clone()); // cast Arc to Arc let res = res.as_arc_any(); - let res = res.downcast::>() + let res = res.downcast::>() .expect("Failure to downcast resource! Does the loader return an `Arc>`?"); + let res = ResHandle::::clone(&res); Ok(res) } else { @@ -112,18 +133,23 @@ impl ResourceManager { /// * `bytes` - The bytes to store. /// /// Returns: The `Arc` to the now stored resource - pub fn load_bytes(&mut self, ident: &str, mime_type: &str, bytes: Vec, offset: usize, length: usize) -> Result>, RequestError> { + pub fn load_bytes(&mut self, ident: &str, mime_type: &str, bytes: Vec, offset: usize, length: usize) -> Result, RequestError> + where + T: Send + Sync + Any + 'static + { if let Some(loader) = self.loaders.iter() .find(|l| l.does_support_mime(mime_type)) { let loader = loader.clone(); let res = loader.load_bytes(self, bytes, offset, length)?; + let res: Arc = Arc::from(res); self.resources.insert(ident.to_string(), res.clone()); // code here... // cast Arc to Arc let res = res.as_arc_any(); - let res = res.downcast::>() + let res = res.downcast::>() .expect("Failure to downcast resource! Does the loader return an `Arc>`?"); + let res = ResHandle::::clone(&res); Ok(res) } else { @@ -132,11 +158,14 @@ impl ResourceManager { } /// Requests bytes from the manager. - pub fn request_loaded_bytes(&mut self, ident: &str) -> Result>, RequestError> { + pub fn request_loaded_bytes(&mut self, ident: &str) -> Result>, RequestError> + where + T: Send + Sync + Any + 'static + { match self.resources.get(&ident.to_string()) { Some(res) => { let res = res.clone().as_arc_any(); - let res = res.downcast::>().expect("Failure to downcast resource"); + let res = res.downcast::>().expect("Failure to downcast resource"); Ok(res) }, @@ -145,6 +174,81 @@ impl ResourceManager { } } } + + /// Start watching a path for changes. Returns a mspc channel that will send events + pub fn watch(&mut self, path: &str, recursive: bool) -> notify::Result>> { + let (send, recv) = crossbeam::channel::bounded(15); + let mut watcher = RecommendedWatcher::new(send, notify::Config::default())?; + + let recurse_mode = match recursive { + true => notify::RecursiveMode::Recursive, + false => notify::RecursiveMode::NonRecursive, + }; + watcher.watch(path.as_ref(), recurse_mode)?; + + let watcher = Arc::new(RwLock::new(watcher)); + let watcher = ResourceWatcher { + watcher, + events_recv: recv.clone(), + }; + + self.watchers.insert(path.to_string(), watcher); + + Ok(recv) + } + + /// Stops watching a path + pub fn stop_watching(&mut self, path: &str) -> notify::Result<()> { + if let Some(watcher) = self.watchers.get(path) { + let mut watcher = watcher.watcher.write().unwrap(); + watcher.unwatch(Path::new(path))?; + } + + 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>> { + self.watchers.get(&path.to_string()) + .map(|w| w.events_recv.clone()) + } + + /// Reloads a resource. The data inside the resource will be updated, the state may + pub fn reload(&mut self, resource: ResHandle) -> Result<(), RequestError> + where + T: Send + Sync + Any + 'static + { + let path = resource.path(); + if let Some(loader) = self.loaders.iter() + .find(|l| l.does_support_file(&path)) { + let loader = Arc::clone(loader); // stop borrowing from self + let loaded = loader.load(self, &path)?; + let loaded = loaded.as_arc_any(); + + let loaded = loaded.downcast::>() + .unwrap(); + let loaded = match Arc::try_unwrap(loaded) { + Ok(v) => v, + Err(_) => panic!("Seems impossible that this would happen, the resource was just loaded!"), + }; + let loaded = loaded.data; + let loaded = match Arc::try_unwrap(loaded) { + Ok(v) => v, + Err(_) => panic!("Seems impossible that this would happen, the resource was just loaded!"), + }; + let loaded = loaded.into_inner().unwrap(); + + let res_lock = &resource.data; + let mut res_lock = res_lock.write().unwrap(); + let version = res_lock.version; + // safe since loaded was JUST loaded, it will be unlocked and not poisoned + *res_lock = loaded; + res_lock.version = version + 1; + } + + Ok(()) + } } #[cfg(test)] @@ -165,20 +269,20 @@ mod tests { fn load_image() { let mut man = ResourceManager::new(); let res = man.request::(&get_image("squiggles.png")).unwrap(); - assert_eq!(res.state, ResourceState::Ready); - let img = res.data.as_ref(); + assert_eq!(res.state(), ResourceState::Ready); + let img = res.try_data_ref(); img.unwrap(); } - /// Ensures that only one copy of the same thing made + /// Ensures that only one copy of the same data was made #[test] fn ensure_single() { let mut man = ResourceManager::new(); let res = man.request::(&get_image("squiggles.png")).unwrap(); - assert_eq!(Arc::strong_count(&res), 2); + assert_eq!(Arc::strong_count(&res.data), 2); let resagain = man.request::(&get_image("squiggles.png")).unwrap(); - assert_eq!(Arc::strong_count(&resagain), 3); + assert_eq!(Arc::strong_count(&resagain.data), 3); } /// Ensures that an error is returned when a file that doesn't exist is requested @@ -196,4 +300,45 @@ mod tests { } ); } + + #[test] + fn reload_image() { + let mut man = ResourceManager::new(); + let res = man.request::(&get_image("squiggles.png")).unwrap(); + assert_eq!(res.state(), ResourceState::Ready); + let img = res.try_data_ref(); + img.unwrap(); + + man.reload(res.clone()).unwrap(); + assert_eq!(res.version(), 1); + + man.reload(res.clone()).unwrap(); + assert_eq!(res.version(), 2); + } + + #[test] + fn watch_image() { + let image_path = get_image("squiggles.png"); + let image_bytes = std::fs::read(&image_path).unwrap(); + + let mut man = ResourceManager::new(); + let res = man.request::(&image_path).unwrap(); + assert_eq!(res.state(), ResourceState::Ready); + let img = res.try_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(); + + std::fs::write(image_path, image_bytes).unwrap(); + + println!("Event kind: {:?}", event.kind); + + // for some reason, + assert!(event.kind.is_remove() || event.kind.is_modify()); + } } \ No newline at end of file