Skip to main content

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}