1pub const MODULE_HOOK: &str = "// unlit_wgpu::hook module";
14pub const VERTEX_HOOK: &str = "// unlit_wgpu::hook vertex";
16pub const FRAGMENT_HOOK: &str = "// unlit_wgpu::hook fragment";
18
19pub fn splice(source: &str, module: &str, vertex: &str, fragment: &str) -> String {
31 let spliced = splice_at(source, MODULE_HOOK, module);
32 let spliced = splice_at(&spliced, VERTEX_HOOK, vertex);
33 let spliced = splice_at(&spliced, FRAGMENT_HOOK, fragment);
34 assert!(
35 !spliced.contains(MODULE_HOOK)
36 && !spliced.contains(VERTEX_HOOK)
37 && !spliced.contains(FRAGMENT_HOOK),
38 "the built-in entry shader carries all three hook marks"
39 );
40 spliced
41}
42
43fn splice_at(source: &str, mark: &str, snippet: &str) -> String {
50 let Some(at) = source.find(mark) else {
51 return source.to_owned();
52 };
53 let line_start = source[..at].rfind('\n').map_or(0, |newline| newline + 1);
54 let line_end = source[at..]
55 .find('\n')
56 .map_or(source.len(), |newline| at + newline);
57 let mut spliced = String::with_capacity(source.len() + snippet.len());
58 spliced.push_str(&source[..line_start]);
59 spliced.push_str(snippet);
60 spliced.push_str(&source[line_end..]);
61 spliced
62}
63
64pub fn compose(
77 module: &str,
78 features: &[(&'static str, bool)],
79 replacement: Option<&str>,
80) -> Result<String, ComposeError> {
81 let main_path = wesl::syntax::ModulePath::new(
82 wesl::syntax::PathOrigin::Package("unlit_wgpu".to_owned()),
83 vec![module.to_owned()],
84 );
85 let mut compile_options = wesl::CompileOptions::default();
86 for (name, enabled) in features {
87 compile_options.features.set(name, *enabled);
88 }
89 compile_options.keep_main = true;
90
91 let mut packages = wesl::resolver::PackageResolver::new();
92 packages.add_package(&crate::shader::PACKAGE);
93
94 let composed = match replacement {
95 None => {
96 wesl::Compiler::new_with_resolver(compile_options, packages).compile_module(&main_path)
97 }
98 Some(source) => {
99 let mut modules: wesl::resolver::VirtualResolver<'static> =
100 wesl::resolver::VirtualResolver::new();
101 modules.add_module(
102 wesl::syntax::ModulePath::new(wesl::syntax::PathOrigin::Absolute, Vec::new()),
103 source.to_owned().into(),
104 );
105 let mut resolver = wesl::resolver::Router::new();
106 resolver.mount_resolver(main_path.clone(), modules);
107 resolver.mount_fallback_resolver(packages);
108 wesl::Compiler::new_with_resolver(compile_options, resolver).compile_module(&main_path)
109 }
110 };
111 composed
112 .map(|result| result.syntax.to_string())
113 .map_err(ComposeError::Compile)
114}
115
116#[derive(Debug, thiserror::Error)]
118pub enum ComposeError {
119 #[error("WESL compilation failed: {0}")]
121 Compile(#[from] wesl::Error),
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
131 fn each_module_keeps_its_entry_point_names() {
132 let vertex =
133 compose("fullscreen_vertex", &[], None).expect("the fullscreen vertex composes");
134 assert!(vertex.contains("fn fullscreen_vertex_shader"));
135
136 let fragment = compose("blit", &[], None).expect("the blit fragment composes");
137 assert!(fragment.contains("fn fs_main"));
138
139 let line = compose("line", &[], None).expect("the line module composes");
140 assert!(line.contains("fn vs_main"));
141 assert!(line.contains("fn fs_main"));
142 }
143}