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`, and report the footprint they cost. The table set itself is
109/// enumerated once in `concinnity_core::resource::install_tables`, shared with
110/// the typed bake builder; what this adds is the load-time accounting. Each
111/// builder MOVES its kind's data bytes out of the records, so the caller's
112/// record vec is spent scaffolding afterwards. Dev-only source catalogues
113/// (hot-reload) stay with the debug path that captures them, not here.
114pub fn install_resource_tables(world: &mut crate::ecs::World, records: &mut [ResourceRecord]) {
115    log_resource_footprint(records);
116    concinnity_core::resource::install_tables(world, records);
117}
118
119// Log the compiled-resource footprint at load: the payload bytes each record
120// references in the blob (resident once the blob's payload section is read) plus
121// the data-resource bytes the tables hold directly. A coarse figure toward the
122// memory budget (see `app::budget`), surfaced at start so the resource load's
123// weight is visible.
124fn log_resource_footprint(records: &[ResourceRecord]) {
125    if records.is_empty() {
126        return;
127    }
128    let total: u64 = records
129        .iter()
130        .map(|r| r.data_bytes.len() as u64 + r.payload.as_ref().map_or(0, |p| p.len))
131        .sum();
132    tracing::info!(
133        "Resource tables: {} record(s), {} MiB compiled",
134        records.len(),
135        total / (1024 * 1024)
136    );
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use concinnity_core::ecs::{PayloadLocator, ResourceKind};
143
144    // Each kind gets a distinct record count, so a table that picked up another
145    // kind's records shows as a wrong length. Order matches `table_lens`.
146    const KIND_COUNTS: [(ResourceKind, u32); 8] = [
147        (ResourceKind::AudioClip, 1),
148        (ResourceKind::Texture, 2),
149        (ResourceKind::ColorLut, 3),
150        (ResourceKind::EnvironmentMap, 4),
151        (ResourceKind::Font, 5),
152        (ResourceKind::Material, 6),
153        (ResourceKind::Mesh, 7),
154        (ResourceKind::SkinnedMesh, 8),
155    ];
156
157    // One record carrying both a payload locator and data bytes, so either
158    // branch of the footprint sum has something to add.
159    fn record(kind: ResourceKind, handle: u32) -> ResourceRecord {
160        ResourceRecord {
161            resource_kind: kind as u8,
162            handle,
163            payload: Some(PayloadLocator {
164                blob_index: 0,
165                offset: handle as u64 * 16,
166                len: 16,
167            }),
168            data_bytes: vec![handle as u8; 4],
169        }
170    }
171
172    fn records() -> Vec<ResourceRecord> {
173        KIND_COUNTS
174            .iter()
175            .flat_map(|&(kind, count)| (0..count).map(move |h| record(kind, h)))
176            .collect()
177    }
178
179    // Every table's length, in `KIND_COUNTS` order. The `expect`s are the
180    // assertion that each kind's table was installed at all.
181    fn table_lens(world: &crate::ecs::World) -> [usize; 8] {
182        [
183            world.resource::<AudioClipTable>().expect("audio").0.len(),
184            world.resource::<TextureTable>().expect("texture").0.len(),
185            world
186                .resource::<ColorLutTable>()
187                .expect("color lut")
188                .0
189                .len(),
190            world
191                .resource::<EnvironmentMapTable>()
192                .expect("env map")
193                .0
194                .len(),
195            world.resource::<FontTable>().expect("font").0.len(),
196            world.resource::<MaterialTable>().expect("material").0.len(),
197            world.resource::<MeshTable>().expect("mesh").0.len(),
198            world
199                .resource::<SkinnedMeshTable>()
200                .expect("skinned")
201                .0
202                .len(),
203        ]
204    }
205
206    // Every per-kind table is installed as a world resource, each holding only
207    // its own kind's records, at their handles.
208    #[test]
209    fn install_wires_every_kind_table_into_the_world() {
210        let mut world = crate::ecs::World::new();
211        install_resource_tables(&mut world, &mut records());
212
213        assert_eq!(table_lens(&world), [1, 2, 3, 4, 5, 6, 7, 8]);
214
215        // A record lands at its own handle rather than its position in the stream.
216        let mesh = world.resource::<MeshTable>().expect("mesh table installed");
217        assert_eq!(mesh.0[6].data_bytes, vec![6; 4]);
218        assert_eq!(mesh.0[6].payload.as_ref().expect("payload kept").offset, 96);
219    }
220
221    // The empty stream (a world with no compiled resources) still installs all
222    // eight tables, each empty, so a system reading its table by handle finds
223    // one rather than a missing resource.
224    #[test]
225    fn install_with_no_records_installs_empty_tables() {
226        let mut world = crate::ecs::World::new();
227        install_resource_tables(&mut world, &mut []);
228        assert_eq!(table_lens(&world), [0; 8]);
229    }
230}