concinnity_core/components/compiled_programs.rs
1//! One compiled shader artifact and the digest-keyed lookup over a set of
2//! them, shared by every asset whose shader source is only complete once a
3//! world is loaded: an `SdfVolume`'s distance field and a `Shader`'s hooks.
4//!
5//! The cook runs slangc and stores what it emitted; the renderer assembles the
6//! source it expects, digests it, and takes a stored artifact only on a match.
7//! A hot-reload edit to an engine template misses every entry and recompiles,
8//! which is the behaviour that makes editing one possible at all.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13/// One compiled artifact, the entries it holds, and the source it came from.
14///
15/// An artifact carries more than one entry where the target allows it: slangc
16/// emits one MSL translation unit for a pair of stages, and the Metal runtime
17/// wants both in one library. DXIL has no such form, so a container there
18/// holds exactly one.
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct CompiledProgram {
21 /// Entry point names this artifact holds, as the shader source spells them.
22 pub entries: Vec<String>,
23 /// `slang_source::source_digest` of the assembled source this artifact was
24 /// built from. A renderer whose assembly digests differently has a template
25 /// the artifact predates and must compile rather than load.
26 pub source_digest: u64,
27 /// The emitted artifact: SPIR-V, a signed DXIL container, or MSL text.
28 pub artifact: Vec<u8>,
29}
30
31/// The artifact holding `entry`, if one was compiled from source matching
32/// `digest`. A mismatch is a stale artifact and reads as absent.
33pub fn artifact<'a>(programs: &'a [CompiledProgram], entry: &str, digest: u64) -> Option<&'a [u8]> {
34 programs
35 .iter()
36 .find(|p| p.source_digest == digest && p.entries.iter().any(|e| e == entry))
37 .map(|p| p.artifact.as_slice())
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use alloc::string::ToString;
44 use alloc::vec;
45
46 fn programs() -> Vec<CompiledProgram> {
47 vec![
48 CompiledProgram {
49 entries: vec!["vertex_main".to_string()],
50 source_digest: 7,
51 artifact: vec![1, 2, 3],
52 },
53 // One artifact holding both stages, the shape the Metal target
54 // takes: a library the runtime pulls two functions out of.
55 CompiledProgram {
56 entries: vec![
57 "vertex_main_bindless".to_string(),
58 "fragment_main_bindless".to_string(),
59 ],
60 source_digest: 9,
61 artifact: vec![4, 5],
62 },
63 ]
64 }
65
66 #[test]
67 fn an_entry_is_found_under_its_own_digest_only() {
68 let p = programs();
69 assert_eq!(artifact(&p, "vertex_main", 7), Some(&[1u8, 2, 3][..]));
70 assert_eq!(artifact(&p, "vertex_main", 8), None, "stale digest");
71 assert_eq!(artifact(&p, "no_such_entry", 7), None);
72 }
73
74 #[test]
75 fn a_shared_artifact_is_found_under_either_entry() {
76 let p = programs();
77 assert_eq!(artifact(&p, "vertex_main_bindless", 9), Some(&[4u8, 5][..]));
78 assert_eq!(
79 artifact(&p, "fragment_main_bindless", 9),
80 Some(&[4u8, 5][..])
81 );
82 }
83
84 #[test]
85 fn a_program_round_trips_through_postcard() {
86 let p = programs();
87 let bytes = postcard::to_allocvec(&p).unwrap();
88 let back: Vec<CompiledProgram> = postcard::from_bytes(&bytes).unwrap();
89 assert_eq!(back, p);
90 }
91}