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
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Family {
24 Surface,
26 Volumetric,
28 Shadow,
30}
31
32impl Family {
33 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Stage {
46 Vertex,
48 Fragment,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Program {
55 pub entry: &'static str,
57 pub stage: Stage,
59 pub family: Family,
61}
62
63pub 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
97pub 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
107pub 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
119pub 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
124pub 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
137pub 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
154pub 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 #[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 #[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 #[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 #[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 #[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}