khora_data/gpu/store.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Unified, engine-wide GPU/asset resource store.
16//!
17//! [`AssetStore`] is the single home for every projected asset cache —
18//! `Assets<GpuMesh>`, `Assets<GpuMaterial>`, `Assets<CpuTexture>`, and any
19//! future type. It replaces the per-type named wrappers (`GpuCache`,
20//! `GpuMaterialCache`, `CpuTextureCache`) that each existed only to get a
21//! distinct `TypeId` in the runtime resource registry.
22//!
23//! It is the **Data** side of CLAD: a store of resources projected from the
24//! ECS, consumed by the render descent. One `AssetStore` is created at
25//! bootstrap, registered into `runtime.resources`, and shared (cheap `Arc`
26//! clone) by the projection and every consumer. Adding a new asset type
27//! needs zero new wiring: `store.store::<T>()` lazily creates the typed
28//! sub-store on first access.
29
30use crate::assets::Assets;
31use khora_core::asset::Asset;
32use std::any::{Any, TypeId};
33use std::collections::HashMap;
34use std::sync::{Arc, Mutex, RwLock};
35
36/// Shared, type-erased registry of `Assets<T>` sub-stores keyed by `TypeId`.
37///
38/// Cloning is cheap (clones an `Arc`); all clones share the same underlying
39/// sub-stores.
40#[derive(Clone, Default)]
41pub struct AssetStore {
42 inner: Arc<Mutex<HashMap<TypeId, Box<dyn Any + Send + Sync>>>>,
43}
44
45impl AssetStore {
46 /// Creates a new, empty store.
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 /// Returns the shared `Assets<T>` sub-store, creating it on first access.
52 ///
53 /// The returned `Arc<RwLock<Assets<T>>>` can be held across frames; it is
54 /// the same handle every caller receives for a given `T`.
55 pub fn store<T: Asset>(&self) -> Arc<RwLock<Assets<T>>> {
56 let mut map = self.inner.lock().expect("AssetStore mutex poisoned");
57 let entry = map
58 .entry(TypeId::of::<T>())
59 .or_insert_with(|| Box::new(Arc::new(RwLock::new(Assets::<T>::new()))));
60 entry
61 .downcast_ref::<Arc<RwLock<Assets<T>>>>()
62 .expect("AssetStore type mismatch for TypeId")
63 .clone()
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use khora_core::asset::AssetUUID;
71
72 #[derive(Debug, PartialEq)]
73 struct Foo(u32);
74 impl Asset for Foo {}
75 struct Bar(#[allow(dead_code)] &'static str);
76 impl Asset for Bar {}
77
78 #[test]
79 fn same_type_returns_shared_substore() {
80 let store = AssetStore::new();
81 let uuid = AssetUUID::new();
82 store
83 .store::<Foo>()
84 .write()
85 .unwrap()
86 .insert(uuid, khora_core::asset::AssetHandle::new(Foo(7)));
87 // A second handle sees the same data.
88 let got = store.store::<Foo>();
89 let guard = got.read().unwrap();
90 assert_eq!(guard.get(&uuid).map(|h| h.0), Some(7));
91 }
92
93 #[test]
94 fn distinct_types_are_independent() {
95 let store = AssetStore::new();
96 let foo_uuid = AssetUUID::new();
97 store
98 .store::<Foo>()
99 .write()
100 .unwrap()
101 .insert(foo_uuid, khora_core::asset::AssetHandle::new(Foo(1)));
102 // Bar sub-store is independent and empty.
103 assert!(store
104 .store::<Bar>()
105 .read()
106 .unwrap()
107 .get(&foo_uuid)
108 .is_none());
109 }
110
111 #[test]
112 fn clone_shares_backing() {
113 let store = AssetStore::new();
114 let clone = store.clone();
115 let uuid = AssetUUID::new();
116 store
117 .store::<Bar>()
118 .write()
119 .unwrap()
120 .insert(uuid, khora_core::asset::AssetHandle::new(Bar("x")));
121 assert!(clone.store::<Bar>().read().unwrap().get(&uuid).is_some());
122 }
123}