Skip to main content

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}