1use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12
13use crate::platform::Platform;
14use crate::render::slang_source;
15use crate::render::uniforms::{BINDLESS_POOL_SIZE, MAX_PROBES};
16
17pub const VERTEX_MARKER: &str = "{SURFACE_VERTEX}";
19pub const FRAGMENT_MARKER: &str = "{SURFACE_FRAGMENT}";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Stage {
25 Vertex,
27 Fragment,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Program {
34 pub file: &'static str,
36 pub entry: &'static str,
38 pub stage: Stage,
40}
41
42pub const ALL: &[Program] = &[
44 Program {
45 file: "main_bindless.slang",
46 entry: "vertex_main_bindless",
47 stage: Stage::Vertex,
48 },
49 Program {
50 file: "main_bindless.slang",
51 entry: "fragment_main_bindless",
52 stage: Stage::Fragment,
53 },
54];
55
56pub fn program(entry: &str) -> Option<&'static Program> {
58 ALL.iter().find(|p| p.entry == entry)
59}
60
61pub fn programs(_platform: Platform) -> impl Iterator<Item = &'static Program> {
63 ALL.iter()
64}
65
66pub fn groups(platform: Platform) -> Vec<Vec<&'static Program>> {
71 if platform == Platform::Metal {
72 return alloc::vec![ALL.iter().collect()];
73 }
74 ALL.iter().map(|p| alloc::vec![p]).collect()
75}
76
77#[derive(Debug, Clone, Copy)]
79pub struct Sources<'a> {
80 pub vertex: Option<&'a str>,
82 pub fragment: &'a str,
84}
85
86impl<'a> Sources<'a> {
87 pub fn splices(&self) -> Vec<(&'static str, &'a str)> {
90 let mut out = Vec::with_capacity(2);
91 if let Some(v) = self.vertex {
92 out.push((VERTEX_MARKER, v));
93 }
94 out.push((FRAGMENT_MARKER, self.fragment));
95 out
96 }
97}
98
99pub fn defines(
105 _program: &Program,
106 platform: Platform,
107 pool_size: usize,
108 probe_count: usize,
109) -> Vec<(&'static str, String)> {
110 let probes = ("MAX_PROBES", MAX_PROBES.to_string());
111 match platform {
112 Platform::Metal => alloc::vec![
113 ("METAL_ABI", "1".into()),
114 ("POOL_SIZE", BINDLESS_POOL_SIZE.to_string()),
115 probes,
116 ],
117 Platform::Hlsl => alloc::vec![("DXIL_ABI", "1".into()), probes],
118 Platform::Glsl => alloc::vec![
119 ("POOL_SIZE", pool_size.to_string()),
120 ("MAX_PROBES", probe_count.to_string()),
121 ],
122 }
123}
124
125pub fn source_with(
129 program: &Program,
130 platform: Platform,
131 pool_size: usize,
132 probe_count: usize,
133 sources: &Sources<'_>,
134 resolve: impl Fn(&str) -> Option<&'static str>,
135) -> String {
136 let defines = defines(program, platform, pool_size, probe_count);
137 let defines: Vec<(&str, &str)> = defines.iter().map(|(k, v)| (*k, v.as_str())).collect();
138 slang_source::assemble_with_splices(program.file, &defines, resolve, &sources.splices())
139}
140
141pub fn source(
143 program: &Program,
144 platform: Platform,
145 pool_size: usize,
146 probe_count: usize,
147 sources: &Sources<'_>,
148) -> String {
149 source_with(
150 program,
151 platform,
152 pool_size,
153 probe_count,
154 sources,
155 crate::render::shaders::embedded,
156 )
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 const SHADE: &str =
164 "float4 shade(VertexOut in, GpuObjectData od) { return float4(1.0, 0.0, 1.0, 1.0); }";
165
166 fn fragment_only() -> Sources<'static> {
167 Sources {
168 vertex: None,
169 fragment: SHADE,
170 }
171 }
172
173 #[test]
176 fn a_grouping_covers_every_entry_of_the_host_once() {
177 for platform in [Platform::Metal, Platform::Hlsl, Platform::Glsl] {
178 let mut grouped: Vec<&str> =
179 groups(platform).iter().flatten().map(|p| p.entry).collect();
180 grouped.sort_unstable();
181 let mut listed: Vec<&str> = programs(platform).map(|p| p.entry).collect();
182 listed.sort_unstable();
183 assert_eq!(grouped, listed, "{platform:?}");
184 }
185 }
186
187 #[test]
190 fn the_pair_is_the_whole_table_and_metal_groups_it() {
191 for platform in [Platform::Metal, Platform::Hlsl, Platform::Glsl] {
192 let entries: Vec<&str> = programs(platform).map(|p| p.entry).collect();
193 assert_eq!(
194 entries,
195 ["vertex_main_bindless", "fragment_main_bindless"],
196 "{platform:?}"
197 );
198 }
199 let metal = groups(Platform::Metal);
200 assert_eq!(metal.len(), 1);
201 assert_eq!(metal[0].len(), 2);
202 for platform in [Platform::Hlsl, Platform::Glsl] {
203 assert_eq!(groups(platform).len(), 2, "{platform:?}");
204 assert!(
205 groups(platform).iter().all(|g| g.len() == 1),
206 "{platform:?}"
207 );
208 }
209 }
210
211 #[test]
212 fn every_entry_is_found_by_name_and_names_are_unique() {
213 for p in ALL {
214 assert_eq!(program(p.entry).map(|q| q.entry), Some(p.entry));
215 }
216 assert!(program("no_such_entry").is_none());
217 let mut names: Vec<&str> = ALL.iter().map(|p| p.entry).collect();
218 names.sort_unstable();
219 names.dedup();
220 assert_eq!(names.len(), ALL.len());
221 }
222
223 #[test]
226 fn the_world_fragment_replaces_the_default_and_the_vertex_default_stays() {
227 let frag = program("fragment_main_bindless").unwrap();
228 let src = source(
229 frag,
230 Platform::Metal,
231 BINDLESS_POOL_SIZE,
232 MAX_PROBES,
233 &fragment_only(),
234 );
235 assert!(src.contains(SHADE));
236 assert!(
237 !src.contains("return shade_surface(in, od);"),
238 "default shade replaced"
239 );
240 assert!(!src.contains(FRAGMENT_MARKER) && !src.contains(VERTEX_MARKER));
241 assert!(src.contains("return project_vertex(model, pos, normal, tangent, color, uv);"));
242
243 let both = Sources {
244 vertex: Some(
245 "VertexOut transform(float4x4 m, float3 p, float3 n, float3 t, float3 c, float2 uv) { return project_vertex(m, p, n, t, c, uv); }",
246 ),
247 fragment: SHADE,
248 };
249 let src = source(frag, Platform::Metal, BINDLESS_POOL_SIZE, MAX_PROBES, &both);
250 assert!(src.contains("VertexOut transform(float4x4 m,"));
251 assert!(!src.contains("return project_vertex(model, pos, normal, tangent, color, uv);"));
252 }
253
254 #[test]
257 fn the_pair_assembles_to_one_text() {
258 let vert = program("vertex_main_bindless").unwrap();
259 let frag = program("fragment_main_bindless").unwrap();
260 let a = source(
261 vert,
262 Platform::Metal,
263 BINDLESS_POOL_SIZE,
264 MAX_PROBES,
265 &fragment_only(),
266 );
267 let b = source(
268 frag,
269 Platform::Metal,
270 BINDLESS_POOL_SIZE,
271 MAX_PROBES,
272 &fragment_only(),
273 );
274 assert_eq!(a, b);
275 assert!(a.contains(SHADE));
276 assert!(a.contains("#define METAL_ABI 1"));
277 }
278
279 #[test]
282 fn defines_follow_the_host() {
283 let frag = program("fragment_main_bindless").unwrap();
284 let names =
285 |platform, pool| -> Vec<(&str, String)> { defines(frag, platform, pool, MAX_PROBES) };
286 assert_eq!(
287 names(Platform::Metal, 0),
288 [
289 ("METAL_ABI", "1".to_string()),
290 ("POOL_SIZE", "1024".to_string()),
291 ("MAX_PROBES", "8".to_string())
292 ]
293 );
294 assert_eq!(
295 names(Platform::Hlsl, 0),
296 [
297 ("DXIL_ABI", "1".to_string()),
298 ("MAX_PROBES", "8".to_string())
299 ]
300 );
301 assert_eq!(
302 names(Platform::Glsl, 37),
303 [
304 ("POOL_SIZE", "37".to_string()),
305 ("MAX_PROBES", "8".to_string())
306 ]
307 );
308 assert_eq!(
310 defines(frag, Platform::Glsl, 37, 4),
311 [
312 ("POOL_SIZE", "37".to_string()),
313 ("MAX_PROBES", "4".to_string())
314 ]
315 );
316 assert_eq!(
317 defines(frag, Platform::Metal, 0, 4),
318 defines(frag, Platform::Metal, 0, MAX_PROBES)
319 );
320 }
321}