Skip to main content

concinnity_core/resource/
handles.rs

1//! Resource handle assignment: the one place a resource's dense per-kind handle
2//! is decided.
3//!
4//! A resource (a mesh, texture, material, ...) is addressed at runtime by an
5//! index into its kind's table, and the table index *is* the handle, so the
6//! order handles are handed out in is load-bearing. Both producers of a world
7//! -- the cook pipeline over authored JSON, and the typed
8//! [`bake`](crate::bake) builder -- assign through this module, so the two can
9//! never drift.
10//!
11//! The rules:
12//!
13//! - Each [`ResourceKind`] counts independently from zero, in declaration
14//!   order.
15//! - Geometry draws from one shared `Mesh` space across all four producers,
16//!   assigned in [`MeshBlock`] order and in declaration order within a block,
17//!   because that is the order the runtime enumerates mesh sources in. The
18//!   trailing [`MeshBlock::Runtime`] block belongs to the world itself and is
19//!   assigned at load time, past everything a build hands out.
20//! - Shaders have a space of their own: a `Shader` is a component rather than a
21//!   resource, but a `Material`'s `shader` reference still bakes to a dense
22//!   declaration-order index.
23
24use alloc::vec::Vec;
25use hashbrown::HashMap;
26
27use crate::ecs::ResourceKind;
28use crate::ecs::asset_id::AssetId;
29
30/// Which block of the shared mesh-source handle space an asset belongs to.
31///
32/// Handles are assigned block by block, so a `.mesh` reference resolves to the
33/// same index the runtime reaches that geometry at while decoding.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
35pub enum MeshBlock {
36    /// A `Mesh` resource, compiled into the blob's resource stream.
37    Mesh,
38    /// A `ProceduralMesh` component, whose payload rides its own def.
39    ProceduralMesh,
40    /// A `VoxelChunk` component.
41    VoxelChunk,
42    /// A mesh-kind `File` component.
43    File,
44    /// Geometry a running world baked for itself at start, whose payload is a
45    /// [`RuntimeMeshPayloads`](super::RuntimeMeshPayloads) entry. Last, so
46    /// minting one moves no handle the build handed out. Neither producer of a
47    /// world assigns into it: the block exists at load time only.
48    Runtime,
49}
50
51impl MeshBlock {
52    /// The block's position in the assignment order.
53    pub fn order(self) -> u8 {
54        self as u8
55    }
56}
57
58/// Per-kind handles assigned to each resource, keyed by its identity.
59#[derive(Debug, Default, Clone)]
60pub struct ResourceHandles {
61    // Next unused handle per kind (the count assigned so far).
62    next: HashMap<u8, u32>,
63    // The handle each resource received.
64    map: HashMap<(u8, AssetId), u32>,
65    // Shader handles, a space of their own.
66    shader_map: HashMap<AssetId, u32>,
67    shader_next: u32,
68}
69
70impl ResourceHandles {
71    /// Give one resource the next handle in its kind's space and record it.
72    /// Declaration order in, dense `0..N` out.
73    pub fn assign(&mut self, kind: ResourceKind, id: AssetId) -> u32 {
74        let next = self.next.entry(kind as u8).or_insert(0);
75        let handle = *next;
76        *next += 1;
77        self.map.insert((kind as u8, id), handle);
78        handle
79    }
80
81    /// The handle a resource received, if it was assigned one.
82    pub fn get(&self, kind: ResourceKind, id: AssetId) -> Option<u32> {
83        self.map.get(&(kind as u8, id)).copied()
84    }
85
86    /// How many handles a kind has assigned: its table length.
87    pub fn count(&self, kind: ResourceKind) -> u32 {
88        self.next.get(&(kind as u8)).copied().unwrap_or(0)
89    }
90
91    /// Assign handles across a world's resources, in the order given. The
92    /// caller has already classified each asset and passes only the resources;
93    /// each kind counts independently from zero.
94    pub fn from_assets(assets: impl IntoIterator<Item = (AssetId, ResourceKind)>) -> Self {
95        let mut handles = Self::default();
96        for (id, kind) in assets {
97            handles.assign(kind, id);
98        }
99        handles
100    }
101
102    /// Assign the shared mesh-source handle space over a world's geometry
103    /// producers, given in declaration order with the block each belongs to.
104    /// Handles go out in block order, declaration order within a block.
105    pub fn assign_mesh_sources(&mut self, sources: impl IntoIterator<Item = (AssetId, MeshBlock)>) {
106        let mut sources: Vec<(AssetId, MeshBlock)> = sources.into_iter().collect();
107        // A stable sort by block keeps declaration order within each one.
108        sources.sort_by_key(|(_, block)| block.order());
109        for (id, _) in sources {
110            self.assign(ResourceKind::Mesh, id);
111        }
112    }
113
114    /// Give one `Shader` the next handle in the shader space and record it.
115    pub fn assign_shader(&mut self, id: AssetId) -> u32 {
116        let handle = self.shader_next;
117        self.shader_next += 1;
118        self.shader_map.insert(id, handle);
119        handle
120    }
121
122    /// The handle a `Shader` received, if it was assigned one.
123    pub fn shader(&self, id: AssetId) -> Option<u32> {
124        self.shader_map.get(&id).copied()
125    }
126
127    /// How many shader handles were assigned.
128    pub fn shader_count(&self) -> u32 {
129        self.shader_next
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn handles_are_dense_per_kind_in_declaration_order() {
139        // An AudioClip lands in its own space, independent of the textures
140        // declared before it.
141        let handles = ResourceHandles::from_assets([
142            (AssetId(10), ResourceKind::Texture),
143            (AssetId(11), ResourceKind::Mesh),
144            (AssetId(12), ResourceKind::Texture),
145            (AssetId(20), ResourceKind::AudioClip),
146            (AssetId(14), ResourceKind::Texture),
147        ]);
148
149        assert_eq!(handles.get(ResourceKind::Texture, AssetId(10)), Some(0));
150        assert_eq!(handles.get(ResourceKind::Texture, AssetId(12)), Some(1));
151        assert_eq!(handles.get(ResourceKind::Texture, AssetId(14)), Some(2));
152        assert_eq!(handles.get(ResourceKind::Mesh, AssetId(11)), Some(0));
153        assert_eq!(handles.get(ResourceKind::AudioClip, AssetId(20)), Some(0));
154
155        assert_eq!(handles.count(ResourceKind::Texture), 3);
156        assert_eq!(handles.count(ResourceKind::Mesh), 1);
157        assert_eq!(handles.count(ResourceKind::Material), 0);
158        assert_eq!(handles.get(ResourceKind::Texture, AssetId(99)), None);
159    }
160
161    #[test]
162    fn the_same_id_in_two_kinds_gets_independent_handles() {
163        let mut handles = ResourceHandles::default();
164        assert_eq!(handles.assign(ResourceKind::Texture, AssetId(1)), 0);
165        assert_eq!(handles.assign(ResourceKind::Mesh, AssetId(1)), 0);
166        assert_eq!(handles.get(ResourceKind::Texture, AssetId(1)), Some(0));
167        assert_eq!(handles.get(ResourceKind::Mesh, AssetId(1)), Some(0));
168    }
169
170    // The load-bearing invariant of the shared mesh space: block order first,
171    // declaration order within a block, whatever order they were declared in.
172    #[test]
173    fn mesh_sources_are_block_ordered_across_kinds() {
174        let mut handles = ResourceHandles::default();
175        handles.assign_mesh_sources([
176            (AssetId(0), MeshBlock::Mesh),
177            (AssetId(1), MeshBlock::ProceduralMesh),
178            (AssetId(2), MeshBlock::VoxelChunk),
179            (AssetId(3), MeshBlock::Mesh),
180            (AssetId(4), MeshBlock::File),
181            (AssetId(5), MeshBlock::ProceduralMesh),
182        ]);
183
184        let h = |id: u32| handles.get(ResourceKind::Mesh, AssetId(id));
185        assert_eq!(h(0), Some(0));
186        assert_eq!(h(3), Some(1));
187        assert_eq!(h(1), Some(2));
188        assert_eq!(h(5), Some(3));
189        assert_eq!(h(2), Some(4));
190        assert_eq!(h(4), Some(5));
191        assert_eq!(handles.count(ResourceKind::Mesh), 6);
192    }
193
194    #[test]
195    fn mesh_blocks_run_mesh_procedural_voxel_file_then_runtime() {
196        assert_eq!(MeshBlock::Mesh.order(), 0);
197        assert_eq!(MeshBlock::ProceduralMesh.order(), 1);
198        assert_eq!(MeshBlock::VoxelChunk.order(), 2);
199        assert_eq!(MeshBlock::File.order(), 3);
200        // The world's own block trails every build-assigned one, so minting
201        // geometry at start cannot move a handle already baked into a Prop.
202        assert_eq!(MeshBlock::Runtime.order(), 4);
203    }
204
205    #[test]
206    fn shaders_count_in_a_space_of_their_own() {
207        let mut handles = ResourceHandles::default();
208        handles.assign(ResourceKind::Material, AssetId(7));
209        assert_eq!(handles.assign_shader(AssetId(7)), 0);
210        assert_eq!(handles.assign_shader(AssetId(8)), 1);
211        assert_eq!(handles.shader(AssetId(7)), Some(0));
212        assert_eq!(handles.shader(AssetId(8)), Some(1));
213        assert_eq!(handles.shader(AssetId(9)), None);
214        assert_eq!(handles.shader_count(), 2);
215    }
216}