concinnity_core/components/sdf_programs.rs
1//! The compiled form of an `SdfVolume`'s distance field: what the cook puts in
2//! the volume's payload and the renderer takes out of it.
3//!
4//! An `SdfVolume` is the one asset whose shader source is only complete once a
5//! world is loaded, because the world authors the field that goes in the middle
6//! of the engine's template. Every other engine shader is a build-time artifact.
7//! Making this one a build-time artifact too is what keeps a shipped player from
8//! needing a shader compiler: the cook runs slangc and stores what it emitted.
9//!
10//! The field text rides along with the artifacts because a compiled artifact is
11//! only usable while the template it was built against still matches. The
12//! renderer assembles the source it expects, digests it, and takes the stored
13//! artifact only on a match; a hot-reload edit to the engine template misses
14//! every entry and recompiles, which is the behaviour that makes editing one
15//! possible at all.
16
17use alloc::string::String;
18use alloc::vec::Vec;
19
20use super::compiled_programs::CompiledProgram;
21
22/// An `SdfVolume`'s payload: the authored field plus every entry the cook
23/// compiled from it.
24#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub struct SdfPrograms {
26 /// The authored distance field, spliced at `{SDF_BODY}`. Kept so a renderer
27 /// that cannot use a stored artifact can still assemble and compile.
28 pub field: String,
29 /// Compiled entries, in the order the cook emitted them.
30 pub programs: Vec<CompiledProgram>,
31}
32
33impl SdfPrograms {
34 /// The artifact holding `entry`, if one was compiled from source matching
35 /// `digest`. A mismatch is a stale artifact and reads as absent.
36 pub fn artifact(&self, entry: &str, digest: u64) -> Option<&[u8]> {
37 super::compiled_programs::artifact(&self.programs, entry, digest)
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use alloc::string::ToString;
45 use alloc::vec;
46
47 fn programs() -> SdfPrograms {
48 SdfPrograms {
49 field: "float map() { return 1.0; }".to_string(),
50 programs: vec![
51 CompiledProgram {
52 entries: vec!["raymarch_vertex".to_string()],
53 source_digest: 7,
54 artifact: vec![1, 2, 3],
55 },
56 // One artifact holding both stages, the shape the Metal target
57 // takes: a library the runtime pulls two functions out of.
58 CompiledProgram {
59 entries: vec![
60 "raymarch_volumetric_vertex".to_string(),
61 "raymarch_volumetric_fragment".to_string(),
62 ],
63 source_digest: 9,
64 artifact: vec![4, 5],
65 },
66 ],
67 }
68 }
69
70 #[test]
71 fn an_entry_resolves_only_against_the_digest_it_was_built_from() {
72 let p = programs();
73 assert_eq!(p.artifact("raymarch_vertex", 7), Some(&[1u8, 2, 3][..]));
74 // Either entry of a two-entry artifact resolves to the same bytes.
75 assert_eq!(
76 p.artifact("raymarch_volumetric_vertex", 9),
77 Some(&[4u8, 5][..])
78 );
79 assert_eq!(
80 p.artifact("raymarch_volumetric_fragment", 9),
81 Some(&[4u8, 5][..])
82 );
83 // The template moved under the artifact: the renderer has to compile.
84 assert_eq!(p.artifact("raymarch_vertex", 8), None);
85 // An entry the cook never emitted, for instance a shadow caster on a
86 // volume that did not declare one.
87 assert_eq!(p.artifact("raymarch_shadow_vertex", 7), None);
88 }
89
90 #[test]
91 fn the_field_and_the_artifacts_round_trip_through_postcard() {
92 let bytes = postcard::to_allocvec(&programs()).unwrap();
93 assert_eq!(
94 postcard::from_bytes::<SdfPrograms>(&bytes).unwrap(),
95 programs()
96 );
97 }
98
99 // A volume whose payload predates the compiled form, or one the cook could
100 // not compile, still carries its field: the renderer falls back to
101 // compiling every entry rather than drawing nothing.
102 #[test]
103 fn a_payload_with_no_artifacts_still_carries_the_field() {
104 let p = SdfPrograms {
105 field: "float map() { return 0.0; }".to_string(),
106 programs: Vec::new(),
107 };
108 assert!(p.artifact("raymarch_fragment", 0).is_none());
109 assert!(!p.field.is_empty());
110 }
111}