1#![deny(missing_docs)]
4#![deny(rustdoc::broken_intra_doc_links)]
5
6mod compute;
7mod graphics;
8mod ray_tracing;
9mod shader;
10
11pub use self::{
12 compute::HotComputePipeline,
13 graphics::HotGraphicsPipeline,
14 ray_tracing::HotRayTracingPipeline,
15 shader::{HotShader, HotShaderBuilder},
16};
17
18use {
19 log::{error, info},
20 notify::{Event, EventKind, RecommendedWatcher, recommended_watcher},
21 shader_prepper::{
22 BoxedIncludeProviderError, IncludeProvider, ResolvedInclude, ResolvedIncludePath,
23 process_file,
24 },
25 shaderc::{CompileOptions, Compiler, ShaderKind, SourceLanguage},
26 std::{
27 collections::HashSet,
28 fs::read_to_string,
29 io::{Error, ErrorKind},
30 path::{Path, PathBuf},
31 sync::{
32 Arc, OnceLock,
33 atomic::{AtomicBool, Ordering},
34 },
35 },
36 vk_graph::driver::{
37 DriverError,
38 shader::{Shader, ShaderBuilder},
39 },
40};
41
42struct CompiledShader {
43 files_included: HashSet<PathBuf>,
44 spirv_code: Vec<u8>,
45}
46
47fn compile_shader(
48 path: impl AsRef<Path>,
49 entry_name: &str,
50 shader_kind: Option<ShaderKind>,
51 additional_opts: Option<&CompileOptions<'_>>,
52) -> anyhow::Result<CompiledShader> {
53 info!("Compiling: {}", path.as_ref().display());
54
55 let path = path.as_ref().to_path_buf();
56 let shader_kind = shader_kind.unwrap_or_else(|| guess_shader_kind(&path));
57
58 #[derive(Default)]
59 struct FileIncludeProvider(HashSet<PathBuf>);
60
61 impl IncludeProvider for FileIncludeProvider {
62 type IncludeContext = PathBuf;
63
64 fn get_include(
65 &mut self,
66 path: &ResolvedIncludePath,
67 ) -> Result<String, BoxedIncludeProviderError> {
68 self.0.insert(PathBuf::from(&path.0));
69
70 Ok(read_to_string(&path.0)?)
71 }
72
73 fn resolve_path(
74 &self,
75 path: &str,
76 context: &Self::IncludeContext,
77 ) -> Result<ResolvedInclude<Self::IncludeContext>, BoxedIncludeProviderError> {
78 let path = context.join(path);
79
80 Ok(ResolvedInclude {
81 resolved_path: ResolvedIncludePath(path.to_str().unwrap_or_default().to_string()),
82 context: path
83 .parent()
84 .map(|path| path.to_path_buf())
85 .unwrap_or_default(),
86 })
87 }
88 }
89
90 let mut file_include_provider = FileIncludeProvider::default();
91 let source_code = process_file(
92 path.to_string_lossy().as_ref(),
93 &mut file_include_provider,
94 PathBuf::new(),
95 )
96 .map_err(|err| {
97 error!("unable to process shader file: {err}");
98
99 Error::new(ErrorKind::InvalidData, err)
100 })?
101 .iter()
102 .map(|chunk| chunk.source.as_str())
103 .collect::<String>();
104 let files_included = file_include_provider.0;
105
106 static COMPILER: OnceLock<Compiler> = OnceLock::new();
107 let spirv_code = COMPILER
108 .get_or_init(|| Compiler::new().expect("invalid shaderc compiler"))
109 .compile_into_spirv(
110 &source_code,
111 shader_kind,
112 &path.to_string_lossy(),
113 entry_name,
114 additional_opts,
115 )
116 .inspect_err(|_| {
117 eprintln!("Shader: {}", path.display());
118
119 for (line_index, line) in source_code.split('\n').enumerate() {
120 let line_number = line_index + 1;
121 eprintln!("{line_number}: {line}");
122 }
123 })?
124 .as_binary_u8()
125 .to_vec();
126
127 Ok(CompiledShader {
128 files_included,
129 spirv_code,
130 })
131}
132
133fn compile_shader_and_watch(
134 shader: &HotShader,
135 watcher: &mut RecommendedWatcher,
136) -> Result<ShaderBuilder, DriverError> {
137 let mut base_shader = Shader::new(shader.stage, shader.compile_and_watch(watcher)?.as_slice());
138
139 base_shader = base_shader.entry_name(shader.entry_name.clone());
140
141 if let Some(specialization) = &shader.specialization {
142 base_shader = base_shader.specialization(specialization.clone());
143 }
144
145 Ok(base_shader)
146}
147
148fn compile_shaders_and_watch(
149 shaders: &[HotShader],
150 watcher: &mut RecommendedWatcher,
151) -> Result<Box<[ShaderBuilder]>, DriverError> {
152 shaders
153 .iter()
154 .map(|shader| compile_shader_and_watch(shader, watcher))
155 .collect()
156}
157
158fn create_watcher(has_changes: &Arc<AtomicBool>) -> RecommendedWatcher {
159 let has_changes = Arc::clone(has_changes);
160
161 recommended_watcher(move |event: notify::Result<Event>| {
162 let event = event.unwrap_or_else(|_| Event::new(EventKind::Any));
163 if matches!(
164 event.kind,
165 EventKind::Any | EventKind::Modify(_) | EventKind::Other
166 ) {
167 has_changes.store(true, Ordering::Relaxed);
168 }
169 })
170 .expect("invalid shader watcher")
171}
172
173fn guess_shader_kind(path: impl AsRef<Path>) -> ShaderKind {
174 match path
175 .as_ref()
176 .extension()
177 .map(|ext| ext.to_string_lossy().to_string())
178 .unwrap_or_default()
179 .as_str()
180 {
181 "comp" => ShaderKind::Compute,
182 "task" => ShaderKind::Task,
183 "mesh" => ShaderKind::Mesh,
184 "vert" => ShaderKind::Vertex,
185 "geom" => ShaderKind::Geometry,
186 "tesc" => ShaderKind::TessControl,
187 "tese" => ShaderKind::TessEvaluation,
188 "frag" => ShaderKind::Fragment,
189 "rgen" => ShaderKind::RayGeneration,
190 "rahit" => ShaderKind::AnyHit,
191 "rchit" => ShaderKind::ClosestHit,
192 "rint" => ShaderKind::Intersection,
193 "rcall" => ShaderKind::Callable,
194 "rmiss" => ShaderKind::Miss,
195 _ => ShaderKind::InferFromSource,
196 }
197}
198
199fn guess_shader_source_language(path: impl AsRef<Path>) -> Option<SourceLanguage> {
200 match path
201 .as_ref()
202 .extension()
203 .map(|ext| ext.to_string_lossy().to_string())
204 .unwrap_or_default()
205 .as_str()
206 {
207 "comp" | "task" | "mesh" | "vert" | "geom" | "tesc" | "tese" | "frag" | "rgen"
208 | "rahit" | "rchit" | "rint" | "rcall" | "rmiss" | "glsl" => Some(SourceLanguage::GLSL),
209 "hlsl" => Some(SourceLanguage::HLSL),
210 _ => None,
211 }
212}
213
214#[cfg(test)]
215mod test {
216 use super::{guess_shader_kind, guess_shader_source_language};
217 use shaderc::{ShaderKind, SourceLanguage};
218
219 #[test]
220 fn guess_shader_kind_from_known_extensions() {
221 assert_eq!(guess_shader_kind("shader.comp"), ShaderKind::Compute);
222 assert_eq!(guess_shader_kind("shader.vert"), ShaderKind::Vertex);
223 assert_eq!(guess_shader_kind("shader.frag"), ShaderKind::Fragment);
224 assert_eq!(guess_shader_kind("shader.rgen"), ShaderKind::RayGeneration);
225 }
226
227 #[test]
228 fn guess_shader_kind_defaults_to_infer_from_source() {
229 assert_eq!(
230 guess_shader_kind("shader.unknown"),
231 ShaderKind::InferFromSource
232 );
233 assert_eq!(guess_shader_kind("shader"), ShaderKind::InferFromSource);
234 }
235
236 #[test]
237 fn guess_shader_source_language_from_known_extensions() {
238 assert_eq!(
239 guess_shader_source_language("shader.comp"),
240 Some(SourceLanguage::GLSL)
241 );
242 assert_eq!(
243 guess_shader_source_language("shader.glsl"),
244 Some(SourceLanguage::GLSL)
245 );
246 assert_eq!(
247 guess_shader_source_language("shader.hlsl"),
248 Some(SourceLanguage::HLSL)
249 );
250 }
251
252 #[test]
253 fn guess_shader_source_language_returns_none_for_unknown_extensions() {
254 assert_eq!(guess_shader_source_language("shader.spv"), None);
255 assert_eq!(guess_shader_source_language("shader"), None);
256 }
257}
258
259macro_rules! pipeline {
260 ($pipeline:ident) => {
261 paste::paste! {
262 impl [<Hot $pipeline>] {
263 fn cache(&self) -> ::std::sync::RwLockReadGuard<'_, HotPipeline<$pipeline>> {
264 self.cache.read().expect("poisoned hot pipeline lock")
265 }
266
267 fn cache_mut(&self) -> ::std::sync::RwLockWriteGuard<'_, HotPipeline<$pipeline>> {
268 self.cache.write().expect("poisoned hot pipeline lock")
269 }
270 }
271
272 impl [<Hot $pipeline>] {
273 pub fn device(&self) -> &Device {
275 &self.device
276 }
277
278 pub fn info(&self) -> [<$pipeline Info>] {
280 self.cache().pipeline.info()
281 }
282
283 pub fn set_debug_name(&self, name: impl AsRef<str>) {
288 self.cache().pipeline.set_debug_name(name);
289 }
290
291 pub fn with_debug_name(self, name: impl AsRef<str>) -> Self {
296 self.set_debug_name(name);
297
298 self
299 }
300 }
301
302 impl<'a> Pipeline<'a> for [<Hot $pipeline>] {
303 type Command = <$pipeline as Pipeline<'a>>::Command;
304
305 fn bind_cmd(self, cmd: Command<'a>) -> Self::Command {
306 self.compile_shader_and_bind_cmd(cmd)
307 }
308 }
309
310 impl<'a> Pipeline<'a> for &'a [<Hot $pipeline>] {
311 type Command = <$pipeline as Pipeline<'a>>::Command;
312
313 fn bind_cmd(self, cmd: Command<'a>) -> Self::Command {
314 self.compile_shader_and_bind_cmd(cmd)
315 }
316 }
317 }
318 };
319}
320
321use pipeline;
322
323macro_rules! pipeline_handle {
324 ($hot:ident) => {
325 impl $hot {
326 pub fn handle(&self) -> ::vk_graph::driver::ash::vk::Pipeline {
328 self.cache().pipeline.handle()
329 }
330 }
331 };
332}
333
334use pipeline_handle;
335
336#[derive(Debug)]
337struct HotPipeline<T> {
338 pipeline: T,
339 watcher: RecommendedWatcher,
340}