# Phase 3c: Asset Manager Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 핸들 기반 에셋 관리 시스템으로 Mesh, Texture 등의 에셋을 타입별로 저장/조회/삭제하고, 참조 카운팅으로 메모리를 관리한다. **Architecture:** 새 `voltex_asset` crate를 만든다. `Handle`는 경량 타입 안전 참조(u32 id + generation). `AssetStorage`는 제네릭 에셋 저장소로 참조 카운팅 지원. `Assets`는 중앙 관리자로 타입별 스토리지를 type-erased로 관리한다. 로딩은 앱에서 기존 파서(parse_obj, parse_bmp)로 에셋을 만들고 `insert`하는 방식. **Tech Stack:** Rust 1.94 **Spec:** `docs/superpowers/specs/2026-03-24-voltex-engine-design.md` Phase 3 (3-3. 에셋 매니저) **스코프 제한:** 비동기 로딩, 핫 리로드는 별도 Phase로 분리. 이번에는 핸들 + 스토리지 + 참조 카운팅만 구현. --- ## File Structure ``` crates/ └── voltex_asset/ ├── Cargo.toml └── src/ ├── lib.rs # 모듈 re-export ├── handle.rs # Handle 제네릭 핸들 ├── storage.rs # AssetStorage 참조 카운팅 스토리지 └── assets.rs # Assets 중앙 관리자 (type-erased) examples/ └── asset_demo/ # 에셋 매니저 통합 데모 (NEW) ├── Cargo.toml └── src/ └── main.rs ``` --- ## Task 1: Handle + AssetStorage **Files:** - Create: `crates/voltex_asset/Cargo.toml` - Create: `crates/voltex_asset/src/lib.rs` - Create: `crates/voltex_asset/src/handle.rs` - Create: `crates/voltex_asset/src/storage.rs` - Modify: `Cargo.toml` (워크스페이스에 voltex_asset 추가) Handle은 에셋을 가리키는 경량 식별자. AssetStorage는 에셋 + 참조 카운트를 관리. - [ ] **Step 1: Cargo.toml 작성** ```toml # crates/voltex_asset/Cargo.toml [package] name = "voltex_asset" version = "0.1.0" edition = "2021" [dependencies] ``` - [ ] **Step 2: 워크스페이스 업데이트** `Cargo.toml` root: members에 `"crates/voltex_asset"` 추가, workspace.dependencies에 `voltex_asset = { path = "crates/voltex_asset" }` 추가. - [ ] **Step 3: handle.rs 작성** ```rust // crates/voltex_asset/src/handle.rs use std::marker::PhantomData; /// 타입 안전 에셋 핸들. 경량 식별자로 에셋을 참조. #[derive(Debug)] pub struct Handle { pub id: u32, pub generation: u32, _marker: PhantomData, } // 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 std::hash::Hash for Handle { fn hash(&self, state: &mut H) { self.id.hash(state); self.generation.hash(state); } } impl Handle { pub(crate) fn new(id: u32, generation: u32) -> Self { Self { id, generation, _marker: PhantomData } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_handle_copy() { let h: Handle = Handle::new(0, 0); let h2 = h; assert_eq!(h, h2); // Copy 확인 } #[test] fn test_handle_eq() { let a: Handle = Handle::new(1, 0); let b: Handle = Handle::new(1, 0); let c: Handle = Handle::new(1, 1); assert_eq!(a, b); assert_ne!(a, c); } } ``` - [ ] **Step 4: storage.rs 작성** ```rust // crates/voltex_asset/src/storage.rs use crate::handle::Handle; use std::any::Any; 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(), } } /// 에셋 추가. 핸들 반환. 초기 ref_count = 1. pub fn insert(&mut self, asset: T) -> Handle { if let Some(id) = self.free_ids.pop() { let idx = id as usize; let generation = match &self.entries[idx] { Some(e) => e.generation + 1, None => 0, }; self.entries[idx] = 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> { let entry = self.entries.get(handle.id as usize)?.as_ref()?; if entry.generation == handle.generation { Some(&entry.asset) } else { None } } /// 핸들로 에셋 가변 참조 pub fn get_mut(&mut self, handle: Handle) -> Option<&mut T> { let entry = self.entries.get_mut(handle.id as usize)?.as_mut()?; if entry.generation == handle.generation { Some(&mut entry.asset) } else { None } } /// 참조 카운트 증가 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; } } } /// 참조 카운트 감소. 0이 되면 에셋 삭제. /// 삭제되었으면 true 반환. pub fn release(&mut self, handle: Handle) -> bool { let idx = handle.id as usize; if let Some(Some(entry)) = self.entries.get_mut(idx) { if entry.generation == handle.generation { entry.ref_count = entry.ref_count.saturating_sub(1); if entry.ref_count == 0 { self.entries[idx] = 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) } /// 모든 에셋 순회 (handle, &asset) pub fn iter(&self) -> impl Iterator, &T)> { self.entries.iter().enumerate().filter_map(|(i, entry)| { entry.as_ref().map(|e| (Handle::new(i as u32, e.generation), &e.asset)) }) } } /// Type erasure trait for Assets manager pub trait AssetStorageDyn: Any { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; 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::*; #[test] fn test_insert_and_get() { let mut storage = AssetStorage::new(); let h = storage.insert("hello".to_string()); assert_eq!(storage.get(h), Some(&"hello".to_string())); } #[test] fn test_get_mut() { let mut storage = AssetStorage::new(); let h = storage.insert(vec![1, 2, 3]); storage.get_mut(h).unwrap().push(4); assert_eq!(storage.get(h).unwrap().len(), 4); } #[test] fn test_release_removes_at_zero() { let mut storage = AssetStorage::new(); let h = storage.insert(42); assert_eq!(storage.len(), 1); let removed = storage.release(h); assert!(removed); assert_eq!(storage.len(), 0); assert_eq!(storage.get(h), None); } #[test] fn test_ref_counting() { let mut storage = AssetStorage::new(); let h = storage.insert(42); assert_eq!(storage.ref_count(h), 1); storage.add_ref(h); assert_eq!(storage.ref_count(h), 2); assert!(!storage.release(h)); // ref_count 2 → 1, not removed assert_eq!(storage.ref_count(h), 1); assert!(storage.release(h)); // ref_count 1 → 0, removed assert_eq!(storage.ref_count(h), 0); } #[test] fn test_stale_handle() { let mut storage = AssetStorage::new(); let h1 = storage.insert(10); storage.release(h1); let h2 = storage.insert(20); // reuse slot, generation+1 assert_eq!(storage.get(h1), None); // old handle invalid assert_eq!(storage.get(h2), Some(&20)); } #[test] fn test_id_reuse() { let mut storage = AssetStorage::new(); let h1 = storage.insert("first"); let id1 = h1.id; storage.release(h1); let h2 = storage.insert("second"); assert_eq!(h2.id, id1); // ID 재사용 assert_eq!(h2.generation, 1); // generation 증가 } #[test] fn test_iter() { let mut storage = AssetStorage::new(); storage.insert(10); storage.insert(20); storage.insert(30); let values: Vec = storage.iter().map(|(_, v)| *v).collect(); assert_eq!(values.len(), 3); assert!(values.contains(&10)); assert!(values.contains(&20)); assert!(values.contains(&30)); } } ``` - [ ] **Step 5: lib.rs 작성** ```rust // crates/voltex_asset/src/lib.rs pub mod handle; pub mod storage; pub use handle::Handle; pub use storage::AssetStorage; ``` - [ ] **Step 6: 테스트 통과 확인** Run: `cargo test -p voltex_asset` Expected: handle 2개 + storage 7개 = 9개 PASS - [ ] **Step 7: 커밋** ```bash git add Cargo.toml crates/voltex_asset/ git commit -m "feat(asset): add voltex_asset crate with Handle and AssetStorage" ``` --- ## Task 2: Assets 중앙 관리자 **Files:** - Create: `crates/voltex_asset/src/assets.rs` - Modify: `crates/voltex_asset/src/lib.rs` Assets는 여러 타입의 AssetStorage를 type-erased로 보관. World처럼 TypeId 기반. - [ ] **Step 1: assets.rs 작성** ```rust // crates/voltex_asset/src/assets.rs use std::any::TypeId; use std::collections::HashMap; use crate::handle::Handle; use crate::storage::{AssetStorage, AssetStorageDyn}; /// 중앙 에셋 관리자. 타입별 AssetStorage를 관리. pub struct Assets { storages: HashMap>, } impl Assets { pub fn new() -> Self { Self { storages: HashMap::new(), } } /// 에셋 추가. 스토리지 자동 등록. pub fn insert(&mut self, asset: T) -> Handle { self.storage_mut::().insert(asset) } /// 핸들로 에셋 참조 pub fn get(&self, handle: Handle) -> Option<&T> { self.storage::()?.get(handle) } /// 핸들로 에셋 가변 참조 pub fn get_mut(&mut self, handle: Handle) -> Option<&mut T> { self.storage_mut::().get_mut(handle) } /// 참조 카운트 증가 pub fn add_ref(&mut self, handle: Handle) { self.storage_mut::().add_ref(handle); } /// 참조 카운트 감소 pub fn release(&mut self, handle: Handle) -> bool { self.storage_mut::().release(handle) } /// 타입별 에셋 수 pub fn count(&self) -> usize { self.storage::().map(|s| s.len()).unwrap_or(0) } /// 타입별 스토리지 읽기 참조 pub fn storage(&self) -> Option<&AssetStorage> { self.storages.get(&TypeId::of::()) .and_then(|s| s.as_any().downcast_ref()) } /// 타입별 스토리지 가변 참조 (없으면 생성) pub fn storage_mut(&mut self) -> &mut AssetStorage { self.storages .entry(TypeId::of::()) .or_insert_with(|| Box::new(AssetStorage::::new())) .as_any_mut() .downcast_mut() .unwrap() } } #[cfg(test)] mod tests { use super::*; #[derive(Debug, PartialEq)] struct MeshData { vertex_count: u32 } #[derive(Debug, PartialEq)] struct TextureData { width: u32, height: u32 } #[test] fn test_insert_and_get_different_types() { let mut assets = Assets::new(); let mesh_h = assets.insert(MeshData { vertex_count: 100 }); let tex_h = assets.insert(TextureData { width: 256, height: 256 }); assert_eq!(assets.get(mesh_h).unwrap().vertex_count, 100); assert_eq!(assets.get(tex_h).unwrap().width, 256); } #[test] fn test_count_per_type() { let mut assets = Assets::new(); assets.insert(MeshData { vertex_count: 10 }); assets.insert(MeshData { vertex_count: 20 }); assets.insert(TextureData { width: 64, height: 64 }); assert_eq!(assets.count::(), 2); assert_eq!(assets.count::(), 1); } #[test] fn test_release_through_assets() { let mut assets = Assets::new(); let h = assets.insert(42_i32); assert_eq!(assets.count::(), 1); assets.release(h); assert_eq!(assets.count::(), 0); } #[test] fn test_ref_counting_through_assets() { let mut assets = Assets::new(); let h = assets.insert("shared".to_string()); assets.add_ref(h); assert!(!assets.release(h)); // 2 → 1 assert!(assets.get(h).is_some()); // still alive assert!(assets.release(h)); // 1 → 0 assert!(assets.get(h).is_none()); } #[test] fn test_storage_access() { let mut assets = Assets::new(); assets.insert(10_i32); assets.insert(20_i32); let storage = assets.storage::().unwrap(); let values: Vec = storage.iter().map(|(_, v)| *v).collect(); assert_eq!(values.len(), 2); } } ``` - [ ] **Step 2: lib.rs 업데이트** ```rust // crates/voltex_asset/src/lib.rs pub mod handle; pub mod storage; pub mod assets; pub use handle::Handle; pub use storage::AssetStorage; pub use assets::Assets; ``` - [ ] **Step 3: 테스트 통과 확인** Run: `cargo test -p voltex_asset` Expected: 9 + 5 = 14개 PASS - [ ] **Step 4: 커밋** ```bash git add crates/voltex_asset/ git commit -m "feat(asset): add Assets central manager with type-erased storage" ``` --- ## Task 3: asset_demo 예제 **Files:** - Create: `examples/asset_demo/Cargo.toml` - Create: `examples/asset_demo/src/main.rs` - Modify: `Cargo.toml` (워크스페이스에 추가) 에셋 매니저를 사용하여 메시와 텍스처를 관리하는 데모. many_cubes를 기반으로 하되, Mesh와 GpuTexture를 Assets에 등록하고 Handle로 참조. - [ ] **Step 1: 워크스페이스 + Cargo.toml** workspace members에 `"examples/asset_demo"` 추가. ```toml # examples/asset_demo/Cargo.toml [package] name = "asset_demo" version = "0.1.0" edition = "2021" [dependencies] voltex_math.workspace = true voltex_platform.workspace = true voltex_renderer.workspace = true voltex_ecs.workspace = true voltex_asset.workspace = true wgpu.workspace = true winit.workspace = true bytemuck.workspace = true pollster.workspace = true env_logger.workspace = true log.workspace = true ``` - [ ] **Step 2: main.rs 작성** 이 데모는 many_cubes와 비슷하지만: 1. `Assets` 매니저에 Mesh를 등록: `let mesh_handle = assets.insert(mesh);` 2. ECS 엔티티가 `Handle` 컴포넌트로 에셋을 참조 3. 렌더링 시 `assets.get(mesh_handle)` 로 Mesh를 가져와 사용 4. 참조 카운팅 데모: 타이틀바에 에셋 수 표시 5. R키: 랜덤 엔티티 10개 despawn (에셋은 다른 엔티티가 참조하므로 유지) 6. 단일 큐브 메시를 모든 엔티티가 공유 파일을 작성하기 전에 `examples/many_cubes/src/main.rs`의 dynamic UBO 패턴을 반드시 읽을 것. 핵심 구조: ```rust struct AppState { // ... assets: Assets, world: World, // mesh_handle은 world의 엔티티 컴포넌트로 저장 } // MeshRef 컴포넌트: Handle를 감싸는 newtype #[derive(Clone, Copy)] struct MeshRef(Handle); ``` 렌더링 루프: ```rust // WorldTransform가 있는 엔티티 + MeshRef 쿼리 let entities = state.world.query2::(); for (_, wt, mesh_ref) in &entities { if let Some(mesh) = state.assets.get(mesh_ref.0) { // set vertex/index buffer, draw } } ``` - [ ] **Step 3: 빌드 + 테스트** Run: `cargo build --workspace` Run: `cargo test --workspace` - [ ] **Step 4: 실행 확인** Run: `cargo run -p asset_demo` Expected: 큐브 그리드가 렌더링됨. R키로 엔티티 삭제 시 메시 에셋은 유지. - [ ] **Step 5: 커밋** ```bash git add Cargo.toml examples/asset_demo/ git commit -m "feat: add asset_demo with Handle-based mesh management" ``` --- ## Phase 3c 완료 기준 체크리스트 - [ ] `cargo build --workspace` 성공 - [ ] `cargo test --workspace` — 모든 테스트 통과 (기존 80 + asset 14 = 94개) - [ ] Handle: Copy, Eq, Hash, generation으로 stale 감지 - [ ] AssetStorage: insert/get/release, 참조 카운팅, ID 재사용 - [ ] Assets: 타입별 스토리지 관리, 자동 등록 - [ ] `cargo run -p asset_demo` — 에셋 핸들로 메시 관리, R키 엔티티 삭제 - [ ] 기존 예제 모두 동작