Skip to main content

concinnity_core/render/slang_programs/
raymarch.rs

1//! What the raymarched SDF volume pass compiles, on every backend.
2//!
3//! The other two tables here are per-backend because the backends compile
4//! different sets. This one is shared: all three compile the same six entries
5//! out of `raymarch.slang`, differing only in the ABI define and in what slangc
6//! is asked to emit. The cook iterates it to compile a volume's field ahead of
7//! time and each renderer iterates it to find what the cook left.
8
9use alloc::string::String;
10
11use crate::platform::Platform;
12use crate::render::slang_source;
13
14/// The shader file every entry below compiles from.
15pub const FILE: &str = "raymarch.slang";
16
17/// The marker a volume's authored distance field is spliced at.
18pub const BODY_MARKER: &str = "{SDF_BODY}";
19
20/// Which of the three draws an entry belongs to. A volume compiles one of the
21/// first two, plus the third when it casts shadows.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Family {
24    /// An opaque surface writing colour and depth.
25    Surface,
26    /// A participating medium blended over the scene.
27    Volumetric,
28    /// A depth-only caster marched from the light side.
29    Shadow,
30}
31
32impl Family {
33    /// The variant define selecting this family.
34    pub fn define(self) -> &'static str {
35        match self {
36            Family::Surface => "RAYMARCH_SURFACE",
37            Family::Volumetric => "RAYMARCH_VOLUMETRIC",
38            Family::Shadow => "RAYMARCH_SHADOW",
39        }
40    }
41}
42
43/// Whether an entry runs at the vertex or the fragment stage.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Stage {
46    /// Rasterises the bounding-box proxy.
47    Vertex,
48    /// Marches the field and writes the draw's output.
49    Fragment,
50}
51
52/// One entry point of one family.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Program {
55    /// Entry point name, as the source spells it.
56    pub entry: &'static str,
57    /// Which stage it compiles for.
58    pub stage: Stage,
59    /// The draw it belongs to.
60    pub family: Family,
61}
62
63/// Every entry `raymarch.slang` declares.
64pub const ALL: &[Program] = &[
65    Program {
66        entry: "raymarch_vertex",
67        stage: Stage::Vertex,
68        family: Family::Surface,
69    },
70    Program {
71        entry: "raymarch_fragment",
72        stage: Stage::Fragment,
73        family: Family::Surface,
74    },
75    Program {
76        entry: "raymarch_volumetric_vertex",
77        stage: Stage::Vertex,
78        family: Family::Volumetric,
79    },
80    Program {
81        entry: "raymarch_volumetric_fragment",
82        stage: Stage::Fragment,
83        family: Family::Volumetric,
84    },
85    Program {
86        entry: "raymarch_shadow_vertex",
87        stage: Stage::Vertex,
88        family: Family::Shadow,
89    },
90    Program {
91        entry: "raymarch_shadow_fragment",
92        stage: Stage::Fragment,
93        family: Family::Shadow,
94    },
95];
96
97/// The ABI define naming a host's binding layout. Vulkan takes the source's
98/// `#else` branch and so needs none, which is why this is an `Option`.
99pub fn abi_define(platform: Platform) -> Option<&'static str> {
100    match platform {
101        Platform::Metal => Some("RAYMARCH_METAL"),
102        Platform::Hlsl => Some("RAYMARCH_DXIL"),
103        Platform::Glsl => None,
104    }
105}
106
107/// The families a volume draws with: its own, plus the shadow caster when it
108/// casts one. A volumetric medium never casts, so the pair is exclusive.
109pub fn families(volumetric: bool, cast_shadows: bool) -> impl Iterator<Item = Family> {
110    let own = if volumetric {
111        Family::Volumetric
112    } else {
113        Family::Surface
114    };
115    let shadow = (cast_shadows && !volumetric).then_some(Family::Shadow);
116    core::iter::once(own).chain(shadow)
117}
118
119/// Every entry a volume with these flags needs compiled.
120pub fn programs(volumetric: bool, cast_shadows: bool) -> impl Iterator<Item = &'static Program> {
121    families(volumetric, cast_shadows).flat_map(|f| ALL.iter().filter(move |p| p.family == f))
122}
123
124/// The variant defines for one family on one host.
125pub fn defines(
126    family: Family,
127    platform: Platform,
128) -> alloc::vec::Vec<(&'static str, &'static str)> {
129    let mut out = alloc::vec::Vec::with_capacity(2);
130    if let Some(abi) = abi_define(platform) {
131        out.push((abi, "1"));
132    }
133    out.push((family.define(), "1"));
134    out
135}
136
137/// The exact source text one family compiles for one host, with `field` spliced
138/// in as the world's distance field. `resolve` lets a hot-reload build prefer
139/// the checkout's copy of the template over the embedded one.
140pub fn source_with(
141    family: Family,
142    platform: Platform,
143    field: &str,
144    resolve: impl Fn(&str) -> Option<&'static str>,
145) -> String {
146    slang_source::assemble_with_splices(
147        FILE,
148        &defines(family, platform),
149        resolve,
150        &[(BODY_MARKER, field)],
151    )
152}
153
154/// The same source from the embedded templates alone.
155pub fn source(family: Family, platform: Platform, field: &str) -> String {
156    source_with(family, platform, field, crate::render::shaders::embedded)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use alloc::vec::Vec;
163
164    #[test]
165    fn a_surface_volume_compiles_its_own_pair_and_nothing_else() {
166        let entries: Vec<&str> = programs(false, false).map(|p| p.entry).collect();
167        assert_eq!(entries, ["raymarch_vertex", "raymarch_fragment"]);
168    }
169
170    #[test]
171    fn a_casting_surface_volume_adds_the_shadow_pair() {
172        let entries: Vec<&str> = programs(false, true).map(|p| p.entry).collect();
173        assert_eq!(
174            entries,
175            [
176                "raymarch_vertex",
177                "raymarch_fragment",
178                "raymarch_shadow_vertex",
179                "raymarch_shadow_fragment"
180            ]
181        );
182    }
183
184    // A medium is integrated, not surfaced, so it has no depth to cast from.
185    // The asset validation forces `cast_shadows` off for one; this makes the
186    // table agree even if an authored volume sets both.
187    #[test]
188    fn a_volumetric_volume_never_compiles_a_shadow_caster() {
189        for cast_shadows in [false, true] {
190            let entries: Vec<&str> = programs(true, cast_shadows).map(|p| p.entry).collect();
191            assert_eq!(
192                entries,
193                ["raymarch_volumetric_vertex", "raymarch_volumetric_fragment"]
194            );
195        }
196    }
197
198    // Vulkan takes the source's `#else` branch, so its only define is the
199    // family; the other two name their binding block as well.
200    #[test]
201    fn the_defines_name_the_abi_only_where_the_source_has_a_branch_for_it() {
202        assert_eq!(
203            defines(Family::Surface, Platform::Metal),
204            [("RAYMARCH_METAL", "1"), ("RAYMARCH_SURFACE", "1")]
205        );
206        assert_eq!(
207            defines(Family::Shadow, Platform::Hlsl),
208            [("RAYMARCH_DXIL", "1"), ("RAYMARCH_SHADOW", "1")]
209        );
210        assert_eq!(
211            defines(Family::Volumetric, Platform::Glsl),
212            [("RAYMARCH_VOLUMETRIC", "1")]
213        );
214    }
215
216    // Every entry the table names appears in the source it claims to come from,
217    // so a renamed entry point fails here rather than at a renderer's init.
218    #[test]
219    fn every_entry_the_table_names_is_declared_in_the_source() {
220        let text = crate::render::shaders::embedded(FILE).expect("raymarch.slang");
221        for program in ALL {
222            assert!(
223                text.contains(program.entry),
224                "{} names no entry in {FILE}",
225                program.entry
226            );
227        }
228        assert!(text.contains(BODY_MARKER), "{FILE} carries no body marker");
229    }
230
231    // The field reaches the assembled source and the defines lead it, on every
232    // host. A family's source must also differ per host, or two backends would
233    // share a cache entry for different binding layouts.
234    #[test]
235    fn the_field_is_spliced_and_the_hosts_assemble_differently() {
236        let field = "float map(float3 p, SdfParams q, float t) { return 1.0; }";
237        let mut seen = Vec::new();
238        for platform in [Platform::Metal, Platform::Hlsl, Platform::Glsl] {
239            let src = source(Family::Surface, platform, field);
240            assert!(src.contains(field), "{platform:?} lost the field");
241            assert!(!src.contains(BODY_MARKER), "{platform:?} left the marker");
242            assert!(src.starts_with("#define "), "{platform:?} defines lead");
243            seen.push(slang_source::source_digest(&src));
244        }
245        seen.dedup();
246        assert_eq!(seen.len(), 3, "two hosts assemble identical source");
247    }
248
249    // Two fields are two sources, which is what keeps the content-addressed
250    // cache from serving one world's volume the artifact of another's.
251    #[test]
252    fn two_fields_assemble_to_two_digests() {
253        let a = source(Family::Surface, Platform::Metal, "// one");
254        let b = source(Family::Surface, Platform::Metal, "// two");
255        assert_ne!(
256            slang_source::source_digest(&a),
257            slang_source::source_digest(&b)
258        );
259    }
260}