concinnity_engine/resource.rs
1// src/resource.rs
2//
3// Engine-side resource-table wiring. The per-kind, handle-indexed tables
4// themselves (an audio clip today; meshes / textures / materials on the Windows
5// follow-up) are renderer-free and live in concinnity-core; this module
6// re-exports them under the historical `crate::resource::*` paths, and adds the
7// engine-only glue: `install_resource_tables`, which builds every table from a
8// compiled blob's resource stream and inserts it as a World resource, plus the
9// dev-only source catalogues the hot-reload path captures.
10
11use concinnity_core::ecs::ResourceRecord;
12
13// The per-kind runtime tables + their shared entry type live in concinnity-core
14// so the physics / audio subsystem crates can reach them; re-export them under
15// the historical `crate::resource::*` paths for every reader (the graphics
16// systems, the editor's in-memory build, the examples' `compile_world`).
17pub use concinnity_core::resource::{
18 AudioClipTable, ColorLutTable, EnvironmentMapTable, FontTable, MaterialTable, MeshTable,
19 ResourceEntry, SkinnedMeshTable, TextureTable,
20};
21
22/// One texture's identity + source file, in `TextureHandle` order. A procedural
23/// texture has an empty `source`. `name_id` is the interned asset name (the same
24/// interner the runtime shares in-process under `cn debug`), used by the runtime
25/// spawn-by-name path without interning at runtime.
26#[derive(Debug, Clone, Default)]
27pub struct TextureSource {
28 /// The interned asset name.
29 pub name_id: u32,
30 /// Authored source path; empty for a procedural texture.
31 pub source: String,
32 /// Index of the image within the source document.
33 pub image_index: u32,
34}
35
36/// Dev-only catalogue of texture source files, indexed by `TextureHandle`,
37/// inserted as a world resource by the in-memory (`cn debug` / editor) build.
38/// `GraphicsSystem::init` reads it to seed the hot-reload watcher now that Texture
39/// is a resource without a drained `source` field. Absent in the shipped disk
40/// runtime, which does not hot-reload; init simply captures no sources then.
41#[derive(Debug, Clone, Default)]
42pub struct TextureSources(pub Vec<TextureSource>);
43
44/// Dev-only source catalogue for the singleton ColorLut, inserted by the in-memory
45/// (`cn debug` / editor) build so `GraphicsSystem::init` can seed the hot-reload
46/// watcher now that ColorLut is a resource without a drained `source` field. The
47/// raw authored source path of the first declared ColorLut, or `None`. Absent in
48/// the shipped disk runtime, which does not hot-reload.
49#[derive(Debug, Clone, Default)]
50pub struct ColorLutSources(pub Option<String>);
51
52/// One file-backed EnvironmentMap's re-bake inputs, captured dev-only so the
53/// hot-reload watcher can re-run the IBL convolution with the same dimensions the
54/// build used (a size change would invalidate the shader's prefilter-mip
55/// assumptions).
56#[derive(Debug, Clone, Default)]
57pub struct EnvironmentMapSourceInfo {
58 /// Authored source path of the environment map.
59 pub source: String,
60 /// Prefilter cube edge in pixels.
61 pub prefilter_face_size: u32,
62 /// Irradiance cube edge in pixels.
63 pub irradiance_face_size: u32,
64 /// Samples per prefilter texel.
65 pub prefilter_samples: u32,
66 /// Radiance clamp applied while prefiltering, to suppress fireflies.
67 pub prefilter_clamp: f32,
68}
69
70/// Dev-only source catalogue for the singleton EnvironmentMap. `Some` only for a
71/// file-backed map (a procedural `generator` has nothing to watch). Mirrors
72/// [`ColorLutSources`]; absent in the shipped disk runtime.
73#[derive(Debug, Clone, Default)]
74pub struct EnvironmentMapSources(pub Option<EnvironmentMapSourceInfo>);
75
76/// One file-backed Mesh's re-import inputs, in `MeshHandle` order. Mirrors
77/// cook's `MeshSourceInfo`; an inline-authored mesh has an empty `source`.
78#[derive(Debug, Clone, Default)]
79pub struct MeshSource {
80 /// Authored source path; empty for an inline-authored mesh.
81 pub source: String,
82 /// Index of the primitive within the source document.
83 pub primitive_index: u32,
84 /// How many LODs the mesh declares, including LOD0.
85 pub lod_levels: u32,
86 /// Camera distance at which each LOD past 0 takes over.
87 pub lod_distances: Vec<f32>,
88}
89
90/// Dev-only catalogue of mesh source files, indexed by `MeshHandle`, inserted as
91/// a world resource by the in-memory (`cn debug` / editor) build so
92/// `GraphicsSystem::init` can seed the hot-reload watcher now that Mesh is a
93/// resource without a drained `source` field. Absent in the shipped disk runtime.
94#[derive(Debug, Clone, Default)]
95pub struct MeshSources(pub Vec<MeshSource>);
96
97/// Dev-only catalogue of material identities, in `MaterialHandle` order: the
98/// interned asset name of each compiled `Material`. A material record carries
99/// its kind and handle, not its name, so this is what lets an editor resolve
100/// the material a Prop edit names to the handle the running world loaded it at.
101/// Inserted by the in-memory (`cn debug` / editor) build, and by the editor's
102/// blob boot from world-lock.json, so a session has it either way it started.
103/// Absent in the shipped disk runtime, which addresses every material by handle.
104#[derive(Debug, Clone, Default)]
105pub struct MaterialNames(pub Vec<u32>);
106
107/// Install every per-kind resource table from a compiled blob's resource stream
108/// into `world`. This is the single place the table set is enumerated: the
109/// shipped runtime (`App::load_blob`), the editor's in-memory build, and the
110/// examples' `compile_world` all call it, so a resource kind that migrates into
111/// the stream gets wired into every host by adding one line here. Systems then
112/// read their table by handle. Each builder MOVES its kind's data bytes out of
113/// the records, so the caller's record vec is spent scaffolding afterwards.
114/// Dev-only source catalogues (hot-reload) stay with the debug path that
115/// captures them, not here.
116pub fn install_resource_tables(world: &mut crate::ecs::World, records: &mut [ResourceRecord]) {
117 log_resource_footprint(records);
118 world.insert_resource(AudioClipTable::from_records(records));
119 world.insert_resource(TextureTable::from_records(records));
120 world.insert_resource(ColorLutTable::from_records(records));
121 world.insert_resource(EnvironmentMapTable::from_records(records));
122 world.insert_resource(FontTable::from_records(records));
123 world.insert_resource(MaterialTable::from_records(records));
124 world.insert_resource(MeshTable::from_records(records));
125 world.insert_resource(SkinnedMeshTable::from_records(records));
126}
127
128// Log the compiled-resource footprint at load: the payload bytes each record
129// references in the blob (resident once the blob's payload section is read) plus
130// the data-resource bytes the tables hold directly. A coarse figure toward the
131// memory budget (see `app::budget`), surfaced at start so the resource load's
132// weight is visible.
133fn log_resource_footprint(records: &[ResourceRecord]) {
134 if records.is_empty() {
135 return;
136 }
137 let total: u64 = records
138 .iter()
139 .map(|r| r.data_bytes.len() as u64 + r.payload.as_ref().map_or(0, |p| p.len))
140 .sum();
141 tracing::info!(
142 "Resource tables: {} record(s), {} MiB compiled",
143 records.len(),
144 total / (1024 * 1024)
145 );
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use concinnity_core::ecs::{PayloadLocator, ResourceKind};
152
153 // Each kind gets a distinct record count, so a table that picked up another
154 // kind's records shows as a wrong length. Order matches `table_lens`.
155 const KIND_COUNTS: [(ResourceKind, u32); 8] = [
156 (ResourceKind::AudioClip, 1),
157 (ResourceKind::Texture, 2),
158 (ResourceKind::ColorLut, 3),
159 (ResourceKind::EnvironmentMap, 4),
160 (ResourceKind::Font, 5),
161 (ResourceKind::Material, 6),
162 (ResourceKind::Mesh, 7),
163 (ResourceKind::SkinnedMesh, 8),
164 ];
165
166 // One record carrying both a payload locator and data bytes, so either
167 // branch of the footprint sum has something to add.
168 fn record(kind: ResourceKind, handle: u32) -> ResourceRecord {
169 ResourceRecord {
170 resource_kind: kind as u8,
171 handle,
172 payload: Some(PayloadLocator {
173 blob_index: 0,
174 offset: handle as u64 * 16,
175 len: 16,
176 }),
177 data_bytes: vec![handle as u8; 4],
178 }
179 }
180
181 fn records() -> Vec<ResourceRecord> {
182 KIND_COUNTS
183 .iter()
184 .flat_map(|&(kind, count)| (0..count).map(move |h| record(kind, h)))
185 .collect()
186 }
187
188 // Every table's length, in `KIND_COUNTS` order. The `expect`s are the
189 // assertion that each kind's table was installed at all.
190 fn table_lens(world: &crate::ecs::World) -> [usize; 8] {
191 [
192 world.resource::<AudioClipTable>().expect("audio").0.len(),
193 world.resource::<TextureTable>().expect("texture").0.len(),
194 world
195 .resource::<ColorLutTable>()
196 .expect("color lut")
197 .0
198 .len(),
199 world
200 .resource::<EnvironmentMapTable>()
201 .expect("env map")
202 .0
203 .len(),
204 world.resource::<FontTable>().expect("font").0.len(),
205 world.resource::<MaterialTable>().expect("material").0.len(),
206 world.resource::<MeshTable>().expect("mesh").0.len(),
207 world
208 .resource::<SkinnedMeshTable>()
209 .expect("skinned")
210 .0
211 .len(),
212 ]
213 }
214
215 // Every per-kind table is installed as a world resource, each holding only
216 // its own kind's records, at their handles.
217 #[test]
218 fn install_wires_every_kind_table_into_the_world() {
219 let mut world = crate::ecs::World::new();
220 install_resource_tables(&mut world, &mut records());
221
222 assert_eq!(table_lens(&world), [1, 2, 3, 4, 5, 6, 7, 8]);
223
224 // A record lands at its own handle rather than its position in the stream.
225 let mesh = world.resource::<MeshTable>().expect("mesh table installed");
226 assert_eq!(mesh.0[6].data_bytes, vec![6; 4]);
227 assert_eq!(mesh.0[6].payload.as_ref().expect("payload kept").offset, 96);
228 }
229
230 // The empty stream (a world with no compiled resources) still installs all
231 // eight tables, each empty, so a system reading its table by handle finds
232 // one rather than a missing resource.
233 #[test]
234 fn install_with_no_records_installs_empty_tables() {
235 let mut world = crate::ecs::World::new();
236 install_resource_tables(&mut world, &mut []);
237 assert_eq!(table_lens(&world), [0; 8]);
238 }
239}