Skip to main content

vk_graph_hot/
lib.rs

1//! Shader hot-reload support for `vk-graph` pipelines.
2
3#![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                /// The device which owns this pipeline.
274                pub fn device(&self) -> &Device {
275                    &self.device
276                }
277
278                /// Gets the information used to create this object.
279                pub fn info(&self) -> [<$pipeline Info>] {
280                    self.cache().pipeline.info()
281                }
282
283                /// Sets the debugging name assigned to this pipeline.
284                ///
285                /// _Note:_ The pipeline name may only be assigned once.
286                /// Subsequent calls will not update the previously set name value.
287                pub fn set_debug_name(&self, name: impl AsRef<str>) {
288                    self.cache().pipeline.set_debug_name(name);
289                }
290
291                /// Sets the debugging name assigned to this pipeline.
292                ///
293                /// _Note:_ The pipeline name may only be assigned once.
294                /// Subsequent calls will not update the previously set name value.
295                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            /// The native Vulkan pipeline handle of this pipeline.
327            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}