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/// The helper an authored `shade` calls to read the scene behind the surface.
21pub const SCENE_TAP: &str = "sampleSceneRefracted";
22
23/// Whether an authored distance field reads the scene behind the surface.
24///
25/// The tap is the only way in: the scene snapshot is reached through this
26/// helper and is named by no other declaration a field can see. A renderer
27/// copies the frame's colour target for the pass only when some visible volume
28/// answers `true` here, so a world of opaque volumes pays nothing.
29///
30/// A field that spells the name in a comment reads as tapping. That is the
31/// conservative direction, and the cost of being wrong is the copy this
32/// existed to skip rather than a black refraction.
33pub fn field_taps_scene(field: &str) -> bool {
34    field.contains(SCENE_TAP)
35}
36
37/// Which of the three draws an entry belongs to. A volume compiles one of the
38/// first two, plus the third when it casts shadows.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Family {
41    /// An opaque surface writing colour and depth.
42    Surface,
43    /// A participating medium blended over the scene.
44    Volumetric,
45    /// A depth-only caster marched from the light side.
46    Shadow,
47}
48
49impl Family {
50    /// The variant define selecting this family.
51    pub fn define(self) -> &'static str {
52        match self {
53            Family::Surface => "RAYMARCH_SURFACE",
54            Family::Volumetric => "RAYMARCH_VOLUMETRIC",
55            Family::Shadow => "RAYMARCH_SHADOW",
56        }
57    }
58}
59
60/// Whether an entry runs at the vertex or the fragment stage.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Stage {
63    /// Rasterises the bounding-box proxy.
64    Vertex,
65    /// Marches the field and writes the draw's output.
66    Fragment,
67}
68
69/// One entry point of one family.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct Program {
72    /// Entry point name, as the source spells it.
73    pub entry: &'static str,
74    /// Which stage it compiles for.
75    pub stage: Stage,
76    /// The draw it belongs to.
77    pub family: Family,
78}
79
80/// Every entry `raymarch.slang` declares.
81pub const ALL: &[Program] = &[
82    Program {
83        entry: "raymarch_vertex",
84        stage: Stage::Vertex,
85        family: Family::Surface,
86    },
87    Program {
88        entry: "raymarch_fragment",
89        stage: Stage::Fragment,
90        family: Family::Surface,
91    },
92    Program {
93        entry: "raymarch_volumetric_vertex",
94        stage: Stage::Vertex,
95        family: Family::Volumetric,
96    },
97    Program {
98        entry: "raymarch_volumetric_fragment",
99        stage: Stage::Fragment,
100        family: Family::Volumetric,
101    },
102    Program {
103        entry: "raymarch_shadow_vertex",
104        stage: Stage::Vertex,
105        family: Family::Shadow,
106    },
107    Program {
108        entry: "raymarch_shadow_fragment",
109        stage: Stage::Fragment,
110        family: Family::Shadow,
111    },
112];
113
114/// The ABI define naming a host's binding layout. Vulkan takes the source's
115/// `#else` branch and so needs none, which is why this is an `Option`.
116pub fn abi_define(platform: Platform) -> Option<&'static str> {
117    match platform {
118        Platform::Metal => Some("RAYMARCH_METAL"),
119        Platform::Hlsl => Some("RAYMARCH_DXIL"),
120        Platform::Glsl => None,
121    }
122}
123
124/// The families a volume draws with: its own, plus the shadow caster when it
125/// casts one. A volumetric medium never casts, so the pair is exclusive.
126pub fn families(volumetric: bool, cast_shadows: bool) -> impl Iterator<Item = Family> {
127    let own = if volumetric {
128        Family::Volumetric
129    } else {
130        Family::Surface
131    };
132    let shadow = (cast_shadows && !volumetric).then_some(Family::Shadow);
133    core::iter::once(own).chain(shadow)
134}
135
136/// Every entry a volume with these flags needs compiled.
137pub fn programs(volumetric: bool, cast_shadows: bool) -> impl Iterator<Item = &'static Program> {
138    families(volumetric, cast_shadows).flat_map(|f| ALL.iter().filter(move |p| p.family == f))
139}
140
141/// The variant defines for one family on one host.
142pub fn defines(
143    family: Family,
144    platform: Platform,
145) -> alloc::vec::Vec<(&'static str, &'static str)> {
146    let mut out = alloc::vec::Vec::with_capacity(2);
147    if let Some(abi) = abi_define(platform) {
148        out.push((abi, "1"));
149    }
150    out.push((family.define(), "1"));
151    out
152}
153
154/// The exact source text one family compiles for one host, with `field` spliced
155/// in as the world's distance field. `resolve` lets a hot-reload build prefer
156/// the checkout's copy of the template over the embedded one.
157pub fn source_with(
158    family: Family,
159    platform: Platform,
160    field: &str,
161    resolve: impl Fn(&str) -> Option<&'static str>,
162) -> String {
163    slang_source::assemble_with_splices(
164        FILE,
165        &defines(family, platform),
166        resolve,
167        &[(BODY_MARKER, field)],
168    )
169}
170
171/// The same source from the embedded templates alone.
172pub fn source(family: Family, platform: Platform, field: &str) -> String {
173    source_with(family, platform, field, crate::render::shaders::embedded)
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use alloc::vec::Vec;
180
181    #[test]
182    fn a_surface_volume_compiles_its_own_pair_and_nothing_else() {
183        let entries: Vec<&str> = programs(false, false).map(|p| p.entry).collect();
184        assert_eq!(entries, ["raymarch_vertex", "raymarch_fragment"]);
185    }
186
187    #[test]
188    fn a_casting_surface_volume_adds_the_shadow_pair() {
189        let entries: Vec<&str> = programs(false, true).map(|p| p.entry).collect();
190        assert_eq!(
191            entries,
192            [
193                "raymarch_vertex",
194                "raymarch_fragment",
195                "raymarch_shadow_vertex",
196                "raymarch_shadow_fragment"
197            ]
198        );
199    }
200
201    // A medium is integrated, not surfaced, so it has no depth to cast from.
202    // The asset validation forces `cast_shadows` off for one; this makes the
203    // table agree even if an authored volume sets both.
204    #[test]
205    fn a_volumetric_volume_never_compiles_a_shadow_caster() {
206        for cast_shadows in [false, true] {
207            let entries: Vec<&str> = programs(true, cast_shadows).map(|p| p.entry).collect();
208            assert_eq!(
209                entries,
210                ["raymarch_volumetric_vertex", "raymarch_volumetric_fragment"]
211            );
212        }
213    }
214
215    // Vulkan takes the source's `#else` branch, so its only define is the
216    // family; the other two name their binding block as well.
217    #[test]
218    fn the_defines_name_the_abi_only_where_the_source_has_a_branch_for_it() {
219        assert_eq!(
220            defines(Family::Surface, Platform::Metal),
221            [("RAYMARCH_METAL", "1"), ("RAYMARCH_SURFACE", "1")]
222        );
223        assert_eq!(
224            defines(Family::Shadow, Platform::Hlsl),
225            [("RAYMARCH_DXIL", "1"), ("RAYMARCH_SHADOW", "1")]
226        );
227        assert_eq!(
228            defines(Family::Volumetric, Platform::Glsl),
229            [("RAYMARCH_VOLUMETRIC", "1")]
230        );
231    }
232
233    // Every entry the table names appears in the source it claims to come from,
234    // so a renamed entry point fails here rather than at a renderer's init.
235    #[test]
236    fn every_entry_the_table_names_is_declared_in_the_source() {
237        let text = crate::render::shaders::embedded(FILE).expect("raymarch.slang");
238        for program in ALL {
239            assert!(
240                text.contains(program.entry),
241                "{} names no entry in {FILE}",
242                program.entry
243            );
244        }
245        assert!(text.contains(BODY_MARKER), "{FILE} carries no body marker");
246    }
247
248    // The tap the detection looks for is the one the helpers declare, so a
249    // rename in the shader fails here rather than by silently making every
250    // refractive volume read a stale scene.
251    #[test]
252    fn the_scene_tap_is_declared_by_the_helpers() {
253        let text =
254            crate::render::shaders::embedded("raymarch_common.slang").expect("raymarch_common");
255        assert!(text.contains(SCENE_TAP), "{SCENE_TAP} declares nothing");
256    }
257
258    // A field that never names the tap is a field the scene copy can skip,
259    // which is the whole of the saving.
260    #[test]
261    fn only_a_field_naming_the_tap_reads_as_tapping() {
262        let opaque = "float map(float3 p, SdfParams q, float t) { return 1.0; }";
263        assert!(!field_taps_scene(opaque));
264        let refractive = "s.transmitted = sampleSceneRefracted(frag_uv, normal, 0.05);";
265        assert!(field_taps_scene(refractive));
266    }
267
268    // The engine template names the tap in its own declaration, so the flag
269    // has to come from the authored field alone: assembled source would read
270    // as tapping for every volume in every world.
271    #[test]
272    fn the_assembled_source_is_not_what_the_flag_reads() {
273        let opaque = "float map(float3 p, SdfParams q, float t) { return 1.0; }";
274        let src = source(Family::Surface, Platform::Metal, opaque);
275        assert!(src.contains(SCENE_TAP), "the template declares the tap");
276        assert!(!field_taps_scene(opaque));
277    }
278
279    // The field reaches the assembled source and the defines lead it, on every
280    // host. A family's source must also differ per host, or two backends would
281    // share a cache entry for different binding layouts.
282    #[test]
283    fn the_field_is_spliced_and_the_hosts_assemble_differently() {
284        let field = "float map(float3 p, SdfParams q, float t) { return 1.0; }";
285        let mut seen = Vec::new();
286        for platform in [Platform::Metal, Platform::Hlsl, Platform::Glsl] {
287            let src = source(Family::Surface, platform, field);
288            assert!(src.contains(field), "{platform:?} lost the field");
289            assert!(!src.contains(BODY_MARKER), "{platform:?} left the marker");
290            assert!(src.starts_with("#define "), "{platform:?} defines lead");
291            seen.push(slang_source::source_digest(&src));
292        }
293        seen.dedup();
294        assert_eq!(seen.len(), 3, "two hosts assemble identical source");
295    }
296
297    // Two fields are two sources, which is what keeps the content-addressed
298    // cache from serving one world's volume the artifact of another's.
299    #[test]
300    fn two_fields_assemble_to_two_digests() {
301        let a = source(Family::Surface, Platform::Metal, "// one");
302        let b = source(Family::Surface, Platform::Metal, "// two");
303        assert_ne!(
304            slang_source::source_digest(&a),
305            slang_source::source_digest(&b)
306        );
307    }
308}