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}