concinnity_core/render/slang_programs/
raymarch.rs1use alloc::string::String;
10
11use crate::platform::Platform;
12use crate::render::slang_source;
13
14pub const FILE: &str = "raymarch.slang";
16
17pub const BODY_MARKER: &str = "{SDF_BODY}";
19
20pub const SCENE_TAP: &str = "sampleSceneRefracted";
22
23pub fn field_taps_scene(field: &str) -> bool {
34 field.contains(SCENE_TAP)
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Family {
41 Surface,
43 Volumetric,
45 Shadow,
47}
48
49impl Family {
50 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Stage {
63 Vertex,
65 Fragment,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct Program {
72 pub entry: &'static str,
74 pub stage: Stage,
76 pub family: Family,
78}
79
80pub 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
114pub 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
124pub 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
136pub 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
141pub 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
154pub 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
171pub 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}