From 9a411e72dab2a0475657d049427848b11f25f146 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 20:29:55 +0900 Subject: [PATCH] feat(asset): add voltex_asset crate with Handle, AssetStorage, and Assets manager Co-Authored-By: Claude Sonnet 4.6 --- Cargo.toml | 2 + crates/voltex_asset/Cargo.toml | 6 + crates/voltex_asset/src/assets.rs | 164 +++++++++++++++++++++ crates/voltex_asset/src/handle.rs | 82 +++++++++++ crates/voltex_asset/src/lib.rs | 7 + crates/voltex_asset/src/storage.rs | 227 +++++++++++++++++++++++++++++ 6 files changed, 488 insertions(+) create mode 100644 crates/voltex_asset/Cargo.toml create mode 100644 crates/voltex_asset/src/assets.rs create mode 100644 crates/voltex_asset/src/handle.rs create mode 100644 crates/voltex_asset/src/lib.rs create mode 100644 crates/voltex_asset/src/storage.rs diff --git a/Cargo.toml b/Cargo.toml index ff15cbb..21fa7cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/voltex_platform", "crates/voltex_renderer", "crates/voltex_ecs", + "crates/voltex_asset", "examples/triangle", "examples/model_viewer", "examples/many_cubes", @@ -16,6 +17,7 @@ voltex_math = { path = "crates/voltex_math" } voltex_platform = { path = "crates/voltex_platform" } voltex_renderer = { path = "crates/voltex_renderer" } voltex_ecs = { path = "crates/voltex_ecs" } +voltex_asset = { path = "crates/voltex_asset" } wgpu = "28.0" winit = "0.30" bytemuck = { version = "1", features = ["derive"] } diff --git a/crates/voltex_asset/Cargo.toml b/crates/voltex_asset/Cargo.toml new file mode 100644 index 0000000..069933c --- /dev/null +++ b/crates/voltex_asset/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "voltex_asset" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/voltex_asset/src/assets.rs b/crates/voltex_asset/src/assets.rs new file mode 100644 index 0000000..98eded4 --- /dev/null +++ b/crates/voltex_asset/src/assets.rs @@ -0,0 +1,164 @@ +use std::any::TypeId; +use std::collections::HashMap; + +use crate::handle::Handle; +use crate::storage::{AssetStorage, AssetStorageDyn}; + +pub struct Assets { + storages: HashMap>, +} + +impl Assets { + pub fn new() -> Self { + Self { + storages: HashMap::new(), + } + } + + fn storage_mut_or_insert(&mut self) -> &mut AssetStorage { + self.storages + .entry(TypeId::of::()) + .or_insert_with(|| Box::new(AssetStorage::::new())) + .as_any_mut() + .downcast_mut::>() + .unwrap() + } + + pub fn insert(&mut self, asset: T) -> Handle { + self.storage_mut_or_insert::().insert(asset) + } + + pub fn get(&self, handle: Handle) -> Option<&T> { + self.storages + .get(&TypeId::of::())? + .as_any() + .downcast_ref::>()? + .get(handle) + } + + pub fn get_mut(&mut self, handle: Handle) -> Option<&mut T> { + self.storages + .get_mut(&TypeId::of::())? + .as_any_mut() + .downcast_mut::>()? + .get_mut(handle) + } + + pub fn add_ref(&mut self, handle: Handle) { + if let Some(storage) = self + .storages + .get_mut(&TypeId::of::()) + .and_then(|s| s.as_any_mut().downcast_mut::>()) + { + storage.add_ref(handle); + } + } + + pub fn release(&mut self, handle: Handle) -> bool { + if let Some(storage) = self + .storages + .get_mut(&TypeId::of::()) + .and_then(|s| s.as_any_mut().downcast_mut::>()) + { + storage.release(handle) + } else { + false + } + } + + pub fn count(&self) -> usize { + self.storages + .get(&TypeId::of::()) + .map(|s| s.count()) + .unwrap_or(0) + } + + pub fn storage(&self) -> Option<&AssetStorage> { + self.storages + .get(&TypeId::of::())? + .as_any() + .downcast_ref::>() + } + + pub fn storage_mut(&mut self) -> &mut AssetStorage { + self.storage_mut_or_insert::() + } +} + +impl Default for Assets { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Mesh { + verts: u32, + } + + struct Texture { + width: u32, + } + + #[test] + fn insert_and_get_different_types() { + let mut assets = Assets::new(); + let hm = assets.insert(Mesh { verts: 3 }); + let ht = assets.insert(Texture { width: 512 }); + assert_eq!(assets.get(hm).unwrap().verts, 3); + assert_eq!(assets.get(ht).unwrap().width, 512); + } + + #[test] + fn count_per_type() { + let mut assets = Assets::new(); + assets.insert(Mesh { verts: 3 }); + assets.insert(Mesh { verts: 6 }); + assets.insert(Texture { width: 512 }); + assert_eq!(assets.count::(), 2); + assert_eq!(assets.count::(), 1); + } + + #[test] + fn release_through_assets() { + let mut assets = Assets::new(); + let h = assets.insert(Mesh { verts: 3 }); + assert_eq!(assets.count::(), 1); + let removed = assets.release(h); + assert!(removed); + assert_eq!(assets.count::(), 0); + assert!(assets.get(h).is_none()); + } + + #[test] + fn ref_counting_through_assets() { + let mut assets = Assets::new(); + let h = assets.insert(Mesh { verts: 3 }); + assets.add_ref(h); + let r1 = assets.release(h); + assert!(!r1); + assert!(assets.get(h).is_some()); + let r2 = assets.release(h); + assert!(r2); + assert!(assets.get(h).is_none()); + } + + #[test] + fn storage_access() { + let mut assets = Assets::new(); + let h = assets.insert(Mesh { verts: 3 }); + { + let s = assets.storage::().unwrap(); + assert_eq!(s.len(), 1); + assert_eq!(s.get(h).unwrap().verts, 3); + } + { + let s = assets.storage_mut::(); + s.get_mut(h).unwrap().verts = 9; + } + assert_eq!(assets.get(h).unwrap().verts, 9); + } +} diff --git a/crates/voltex_asset/src/handle.rs b/crates/voltex_asset/src/handle.rs new file mode 100644 index 0000000..14ffddc --- /dev/null +++ b/crates/voltex_asset/src/handle.rs @@ -0,0 +1,82 @@ +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; + +pub struct Handle { + pub(crate) id: u32, + pub(crate) generation: u32, + _marker: PhantomData, +} + +impl Handle { + pub(crate) fn new(id: u32, generation: u32) -> Self { + Self { + id, + generation, + _marker: PhantomData, + } + } +} + +impl Clone for Handle { + fn clone(&self) -> Self { + Self { + id: self.id, + generation: self.generation, + _marker: PhantomData, + } + } +} + +impl Copy for Handle {} + +impl PartialEq for Handle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.generation == other.generation + } +} + +impl Eq for Handle {} + +impl Hash for Handle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.generation.hash(state); + } +} + +impl fmt::Debug for Handle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Handle") + .field("id", &self.id) + .field("generation", &self.generation) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Dummy; + + #[test] + fn test_handle_copy() { + let h: Handle = Handle::new(0, 0); + let h2 = h; // copy + let h3 = h; // still usable after copy + assert_eq!(h2, h3); + } + + #[test] + fn test_handle_eq() { + let h1: Handle = Handle::new(1, 2); + let h2: Handle = Handle::new(1, 2); + let h3: Handle = Handle::new(1, 3); + let h4: Handle = Handle::new(2, 2); + + assert_eq!(h1, h2); + assert_ne!(h1, h3); + assert_ne!(h1, h4); + } +} diff --git a/crates/voltex_asset/src/lib.rs b/crates/voltex_asset/src/lib.rs new file mode 100644 index 0000000..33af7e5 --- /dev/null +++ b/crates/voltex_asset/src/lib.rs @@ -0,0 +1,7 @@ +pub mod handle; +pub mod storage; +pub mod assets; + +pub use handle::Handle; +pub use storage::AssetStorage; +pub use assets::Assets; diff --git a/crates/voltex_asset/src/storage.rs b/crates/voltex_asset/src/storage.rs new file mode 100644 index 0000000..e1d2f88 --- /dev/null +++ b/crates/voltex_asset/src/storage.rs @@ -0,0 +1,227 @@ +use std::any::Any; + +use crate::handle::Handle; + +struct AssetEntry { + asset: T, + generation: u32, + ref_count: u32, +} + +pub struct AssetStorage { + entries: Vec>>, + free_ids: Vec, +} + +impl AssetStorage { + pub fn new() -> Self { + Self { + entries: Vec::new(), + free_ids: Vec::new(), + } + } + + pub fn insert(&mut self, asset: T) -> Handle { + if let Some(id) = self.free_ids.pop() { + let generation = self.entries[id as usize] + .as_ref() + .map(|e| e.generation) + .unwrap_or(0) + + 1; + self.entries[id as usize] = Some(AssetEntry { + asset, + generation, + ref_count: 1, + }); + Handle::new(id, generation) + } else { + let id = self.entries.len() as u32; + self.entries.push(Some(AssetEntry { + asset, + generation: 0, + ref_count: 1, + })); + Handle::new(id, 0) + } + } + + pub fn get(&self, handle: Handle) -> Option<&T> { + self.entries + .get(handle.id as usize)? + .as_ref() + .filter(|e| e.generation == handle.generation) + .map(|e| &e.asset) + } + + pub fn get_mut(&mut self, handle: Handle) -> Option<&mut T> { + self.entries + .get_mut(handle.id as usize)? + .as_mut() + .filter(|e| e.generation == handle.generation) + .map(|e| &mut e.asset) + } + + pub fn add_ref(&mut self, handle: Handle) { + if let Some(Some(entry)) = self.entries.get_mut(handle.id as usize) { + if entry.generation == handle.generation { + entry.ref_count += 1; + } + } + } + + /// Decrements the ref_count. Returns true if the asset was removed. + pub fn release(&mut self, handle: Handle) -> bool { + if let Some(slot) = self.entries.get_mut(handle.id as usize) { + if let Some(entry) = slot.as_mut() { + if entry.generation == handle.generation { + entry.ref_count = entry.ref_count.saturating_sub(1); + if entry.ref_count == 0 { + *slot = None; + self.free_ids.push(handle.id); + return true; + } + } + } + } + false + } + + pub fn len(&self) -> usize { + self.entries.iter().filter(|e| e.is_some()).count() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn ref_count(&self, handle: Handle) -> u32 { + self.entries + .get(handle.id as usize) + .and_then(|e| e.as_ref()) + .filter(|e| e.generation == handle.generation) + .map(|e| e.ref_count) + .unwrap_or(0) + } + + pub fn iter(&self) -> impl Iterator, &T)> { + self.entries + .iter() + .enumerate() + .filter_map(|(id, slot)| { + slot.as_ref().map(|e| (Handle::new(id as u32, e.generation), &e.asset)) + }) + } +} + +impl Default for AssetStorage { + fn default() -> Self { + Self::new() + } +} + +/// Trait for type-erased access to an AssetStorage. +pub trait AssetStorageDyn: Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + /// Number of live assets in this storage. + fn count(&self) -> usize; +} + +impl AssetStorageDyn for AssetStorage { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn count(&self) -> usize { + self.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Mesh { + verts: u32, + } + + #[test] + fn insert_and_get() { + let mut storage: AssetStorage = AssetStorage::new(); + let h = storage.insert(Mesh { verts: 3 }); + assert_eq!(storage.get(h).unwrap().verts, 3); + } + + #[test] + fn get_mut() { + let mut storage: AssetStorage = AssetStorage::new(); + let h = storage.insert(Mesh { verts: 3 }); + storage.get_mut(h).unwrap().verts = 6; + assert_eq!(storage.get(h).unwrap().verts, 6); + } + + #[test] + fn release_removes_at_zero() { + let mut storage: AssetStorage = AssetStorage::new(); + let h = storage.insert(Mesh { verts: 3 }); + assert_eq!(storage.len(), 1); + let removed = storage.release(h); + assert!(removed); + assert_eq!(storage.len(), 0); + assert!(storage.get(h).is_none()); + } + + #[test] + fn ref_counting() { + let mut storage: AssetStorage = AssetStorage::new(); + let h = storage.insert(Mesh { verts: 3 }); + storage.add_ref(h); + assert_eq!(storage.ref_count(h), 2); + let removed1 = storage.release(h); + assert!(!removed1); + assert_eq!(storage.ref_count(h), 1); + let removed2 = storage.release(h); + assert!(removed2); + assert!(storage.get(h).is_none()); + } + + #[test] + fn stale_handle() { + let mut storage: AssetStorage = AssetStorage::new(); + let h = storage.insert(Mesh { verts: 3 }); + storage.release(h); + // h is now stale; get should return None + assert!(storage.get(h).is_none()); + } + + #[test] + fn id_reuse() { + let mut storage: AssetStorage = AssetStorage::new(); + let h1 = storage.insert(Mesh { verts: 3 }); + storage.release(h1); + let h2 = storage.insert(Mesh { verts: 9 }); + // Same slot reused but different generation + assert_eq!(h1.id, h2.id); + assert_ne!(h1.generation, h2.generation); + assert!(storage.get(h1).is_none()); + assert_eq!(storage.get(h2).unwrap().verts, 9); + } + + #[test] + fn iter() { + let mut storage: AssetStorage = AssetStorage::new(); + let h1 = storage.insert(Mesh { verts: 3 }); + let h2 = storage.insert(Mesh { verts: 6 }); + let mut verts: Vec = storage.iter().map(|(_, m)| m.verts).collect(); + verts.sort(); + assert_eq!(verts, vec![3, 6]); + // handles from iter should be usable + let handles: Vec> = storage.iter().map(|(h, _)| h).collect(); + assert!(handles.contains(&h1)); + assert!(handles.contains(&h2)); + } +}