Skip to main content

concinnity_cook/pipeline/
result.rs

1//! What a pipeline run hands back: the compiled defs and packed blobs, plus the
2//! dev-only source catalogues a `cn debug` build reads back by handle.
3
4use concinnity_core::blob::{MeshBoundsRecord, PhysicsBudgetRecord, ResourceKind, SceneGroup};
5
6use crate::ecs::{BlobAssetDef, ResourceRecord};
7
8/// A texture's identity + on-disk source, in `TextureHandle` order. Now that
9/// Texture is a resource (no `source`/`asset_id` on a component the renderer
10/// drains), this is how a dev build hands the `cn debug` tools what they need: the
11/// hot-reload watcher maps `source` -> handle, and the runtime spawn-by-name path
12/// maps `name_id` -> handle. `source` is empty for a procedural texture (nothing
13/// to watch). `name_id` is the interned asset name (same interner the runtime
14/// shares in-process under `cn debug`), so nothing is interned at runtime.
15#[derive(Debug, Clone, Default, PartialEq)]
16pub struct TextureSourceInfo {
17    /// The interned asset name.
18    pub name_id: u32,
19    /// Authored source path; empty for a procedural texture.
20    pub source: String,
21    /// Index of the image within the source document.
22    pub image_index: u32,
23}
24
25/// A file-backed Mesh's re-import inputs, in `MeshHandle` order (the Mesh block
26/// leads the shared mesh-source handle space, so Mesh handles are dense from 0).
27/// Now that Mesh is a resource (no `source` on a component the renderer drains),
28/// this is how a dev build hands the `cn debug` hot-reload watcher what it needs
29/// to re-import a saved `.glb`/`.fbx`. `source` is empty for an inline-authored
30/// mesh (nothing to watch).
31#[derive(Debug, Clone, Default, PartialEq)]
32pub struct MeshSourceInfo {
33    /// Authored source path; empty for an inline-authored mesh.
34    pub source: String,
35    /// Index of the primitive within the source document.
36    pub primitive_index: u32,
37    /// How many LODs the mesh declares, including LOD0.
38    pub lod_levels: u32,
39    /// Camera distance at which each LOD past 0 takes over.
40    pub lod_distances: Vec<f32>,
41}
42
43/// The in-memory result of a complete build pipeline run.
44/// Defs have payload locators filled in; `payloads[i]` is the raw bytes for
45/// blob i. This can be used directly without touching disk.
46pub struct PipelineResult {
47    /// The compiled component defs, with payload locators filled in.
48    pub defs: Vec<BlobAssetDef>,
49    /// Asset name of each def, index-aligned with `defs` (defs only carry the
50    /// interned id; the lock file records the readable name).
51    pub names: Vec<String>,
52    /// The blob's resource stream: compiled resources addressed by their dense
53    /// per-kind handle, carried alongside the component defs. Empty until a
54    /// resource kind migrates off the component registry (AudioClip first).
55    pub resources: Vec<ResourceRecord>,
56    /// Per-scene exclusively-owned blob content, in scene declaration order.
57    pub scene_groups: Vec<SceneGroup>,
58    /// Baked AABB + counts per static mesh payload, by mesh-source handle.
59    pub mesh_bounds: Vec<MeshBoundsRecord>,
60    /// The world's physics reservation, or `None` when it runs no physics.
61    pub physics_budget: Option<PhysicsBudgetRecord>,
62    // Unified mesh-source handle -> asset name for mesh payloads compiled as
63    // component defs (ProceduralMesh and friends). Resource-stream Mesh
64    // handles lead the space and resolve through `resources`; these resolve
65    // through `names`/`defs`. Consumed by the thumbnail baker to compose a
66    // Model's sub-meshes.
67    pub(crate) mesh_component_names: Vec<(u32, String)>,
68    /// Raw bytes of each blob, indexed by blob number.
69    pub payloads: Vec<Vec<u8>>,
70    // Compiled-asset payloads served from the build cache this run.
71    pub(crate) cache_hits: usize,
72    // Compiled-asset payloads compiled fresh this run.
73    pub(crate) cache_misses: usize,
74    /// File-backed texture sources in `TextureHandle` order, for the `cn debug`
75    /// hot-reload watcher. Dev-only info; not written to the shipped blob.
76    pub texture_sources: Vec<TextureSourceInfo>,
77    /// File-backed mesh sources in `MeshHandle` order (dense over the Mesh block
78    /// of the shared mesh-source space), for the `cn debug` hot-reload watcher.
79    /// Dev-only info; not written to the shipped blob.
80    pub mesh_sources: Vec<MeshSourceInfo>,
81    // Lock-file provenance for the resource stream, index-aligned with
82    // `resources` (records only carry the kind tag + handle; the lock records
83    // the readable name and args hash).
84    pub(crate) resource_locks: Vec<crate::blob::LockedResource>,
85}
86
87impl PipelineResult {
88    /// The interned asset name of every compiled resource of `kind`, dense by
89    /// its per-kind handle: the identity a runtime that addresses resources by
90    /// handle has no other way to recover (a resource record carries its kind
91    /// and handle, not its name). 0 where the build recorded no id.
92    pub fn resource_names(&self, kind: ResourceKind) -> Vec<u32> {
93        let mut names = Vec::new();
94        for (record, lock) in self.resources.iter().zip(self.resource_locks.iter()) {
95            if record.resource_kind != kind as u8 {
96                continue;
97            }
98            let slot = record.handle as usize;
99            if names.len() <= slot {
100                names.resize(slot + 1, 0);
101            }
102            names[slot] = lock.id.unwrap_or_default();
103        }
104        names
105    }
106
107    /// The compiled payload bytes of the resource of `kind` declared under
108    /// `name`, sliced out of the in-memory blob sections. `None` when no such
109    /// resource was compiled or it carries no payload. The editor's glTF
110    /// export reads a SkinnedMesh's composed geometry through this.
111    pub fn resource_payload(&self, kind: ResourceKind, name: &str) -> Option<&[u8]> {
112        let record = self
113            .resources
114            .iter()
115            .zip(self.resource_locks.iter())
116            .find(|(r, l)| r.resource_kind == kind as u8 && l.name == name)?
117            .0;
118        let loc = record.payload.as_ref()?;
119        let blob = self.payloads.get(loc.blob_index as usize)?;
120        let start = usize::try_from(loc.offset).ok()?;
121        let end = start.checked_add(usize::try_from(loc.len).ok()?)?;
122        blob.get(start..end)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::pipeline::build_pipeline_from_str;
130
131    #[test]
132    fn resource_payload_slices_the_named_resource() {
133        let world = concat!(
134            r#"{"name":"prism","type":"SkinnedMesh","args":{"#,
135            r#""vertices":[{"pos":[0,0,0]},{"pos":[1,0,0]},{"pos":[0,1,0]}],"#,
136            r#""indices":[0,1,2],"skeleton":[{"name":"root","parent":-1}],"#,
137            r#""scale":[1,1,1]}}"#,
138            "\n",
139        );
140        let result = build_pipeline_from_str(
141            world,
142            None,
143            None,
144            concinnity_core::platform::Platform::Metal,
145        )
146        .expect("build");
147        let bytes = result
148            .resource_payload(ResourceKind::SkinnedMesh, "prism")
149            .expect("named payload");
150        let payload =
151            concinnity_core::gfx::mesh_payload::deserialise_skinned_with_lods(bytes).unwrap();
152        assert_eq!(payload.vertices.len(), 3);
153        assert_eq!(payload.joints[0].name, "root");
154        // The wrong name or the wrong kind finds nothing.
155        assert!(
156            result
157                .resource_payload(ResourceKind::SkinnedMesh, "ghost")
158                .is_none()
159        );
160        assert!(
161            result
162                .resource_payload(ResourceKind::Texture, "prism")
163                .is_none()
164        );
165    }
166}