Skip to main content

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}