Skip to main content

khal_builder/
lib.rs

1//! Build-time utilities for compiling shader crates to SPIR-V and PTX.
2
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6/// Configures and runs the SPIR-V and PTX shader compilation pipeline.
7///
8/// Used in `build.rs` scripts to compile a shader crate before the host crate.
9pub struct KhalBuilder {
10    shader_crate: PathBuf,
11    // Useful for unusual crates layout where the src directory isn’t in `shader_crate/src`.
12    shader_src: Option<PathBuf>,
13    // Features to enable when building the library.
14    features: Vec<String>,
15    // The `RUST_MIN_STACK` given to the shader builders.
16    rust_min_stack: u32,
17    /// If the `cuda` feature is enabled and this is `true`, then cuda PTX kernels will be built with cargo-cuda.
18    /// Default: `true`
19    #[allow(dead_code)]
20    build_cuda: bool,
21    /// If this is `true`, then SpirV kernels will be built with cargo-gpu.
22    /// Default: `true`
23    build_spirv: bool,
24}
25
26impl KhalBuilder {
27    /// Creates a new builder for the given shader crate directory.
28    /// If `enable_builtin_features` is true, platform-specific features are auto-detected.
29    pub fn new(shader_crate: impl AsRef<Path>, enable_builtin_features: bool) -> Self {
30        let mut builder = Self {
31            shader_crate: shader_crate.as_ref().to_owned(),
32            shader_src: None,
33            features: Vec::new(),
34            build_cuda: true,
35            build_spirv: true,
36            rust_min_stack: 1024 * 1024 * 32,
37        };
38        if enable_builtin_features {
39            builder = builder.append_builtin_features();
40        }
41        builder
42    }
43
44    /// Creates a new builder by locating the shader crate via cargo's `links`
45    /// metadata mechanism.
46    ///
47    /// `links_name` must match the `links` value declared in the shader
48    /// crate's `Cargo.toml`. The shader crate's `build.rs` must emit
49    /// `cargo::metadata=manifest_dir=$CARGO_MANIFEST_DIR`, and the host crate
50    /// must depend on the shader crate as a `[build-dependencies]` entry.
51    /// Cargo then exposes `DEP_<LINKS>_MANIFEST_DIR` to this build script,
52    /// which works identically for in-workspace path dependencies and for
53    /// versions fetched from a registry.
54    pub fn from_dependency(links_name: &str, enable_builtin_features: bool) -> Self {
55        let env_key = format!(
56            "DEP_{}_MANIFEST_DIR",
57            links_name.to_ascii_uppercase().replace('-', "_")
58        );
59        let manifest_dir = std::env::var(&env_key).unwrap_or_else(|_| {
60            panic!(
61                "environment variable `{env_key}` is not set; ensure `{links_name}` is declared \
62                 as a `[build-dependencies]` entry of the host crate and that its `build.rs` emits \
63                 `cargo::metadata=manifest_dir=$CARGO_MANIFEST_DIR`"
64            )
65        });
66        Self::new(manifest_dir, enable_builtin_features)
67    }
68
69    /// Sets the `RUST_MIN_STACK` environment variable for the shader compilation processes.
70    pub fn rust_min_stack(mut self, stack: u32) -> Self {
71        self.rust_min_stack = stack;
72        self
73    }
74
75    /// Overrides the shader source directory (defaults to `<shader_crate>/src`).
76    pub fn shader_src(mut self, src: impl AsRef<Path>) -> Self {
77        self.shader_src = Some(src.as_ref().to_owned());
78        self
79    }
80
81    /// Adds a cargo feature to enable when building the shader crate.
82    pub fn feature(mut self, feature: impl ToString) -> Self {
83        let feature = feature.to_string();
84        if !self.features.contains(&feature) {
85            self.features.push(feature);
86        }
87        self
88    }
89
90    /// Compiles the shader crate and writes output files to `output_dir`.
91    pub fn build(self, output_dir: impl AsRef<Path>) {
92        let output_dir = output_dir.as_ref();
93
94        self.setup_change_detection();
95
96        if self.build_spirv {
97            self.build_spirv(output_dir);
98        }
99
100        #[cfg(feature = "cuda")]
101        if self.build_cuda {
102            self.build_ptx(output_dir);
103        }
104    }
105
106    fn append_builtin_features(mut self) -> Self {
107        if cfg!(feature = "unsafe_remove_boundchecks") {
108            self = self.feature("unsafe-remove-boundchecks");
109        }
110
111        self
112    }
113
114    fn setup_change_detection(&self) {
115        println!(
116            "cargo:rerun-if-changed={}",
117            self.shader_crate.to_string_lossy()
118        );
119        let shader_src = self
120            .shader_src
121            .clone()
122            .unwrap_or_else(|| self.shader_crate.join("src"));
123        for entry in walkdir::WalkDir::new(shader_src)
124            .into_iter()
125            .filter_map(|e| e.ok())
126        {
127            println!("cargo:rerun-if-changed={}", entry.path().display());
128        }
129
130        println!("cargo:rerun-if-env-changed=CARGO_FEATURE_PUSH_CONSTANTS"); // TODO: currently unused
131        println!("cargo:rerun-if-env-changed=CARGO_FEATURE_CUDA");
132    }
133
134    fn build_spirv(&self, output_dir: impl AsRef<Path>) {
135        let output_dir = output_dir.as_ref();
136        let mut args = vec![
137            "gpu",
138            "build",
139            "--shader-crate",
140            self.shader_crate
141                .to_str()
142                .expect("Invalid shader crate path"),
143            "--output-dir",
144            output_dir.to_str().expect("Invalid output directory path"),
145            "--multimodule",
146        ];
147
148        let features_str = self.features.join(",");
149        if !features_str.is_empty() {
150            args.push("--features");
151            args.push(&features_str);
152        }
153
154        let status = Command::new("cargo")
155            .args(args)
156            .env("RUST_MIN_STACK", self.rust_min_stack.to_string())
157            .status()
158            .expect("failed to run cargo gpu");
159
160        if !status.success() {
161            panic!("cargo gpu build failed");
162        }
163    }
164
165    /// Compiles the shader crate to PTX for the CUDA backend.
166    #[cfg(feature = "cuda")]
167    fn build_ptx(&self, output_dir: impl AsRef<Path>) {
168        let output_dir = output_dir.as_ref();
169        let features_str = self.features.join(",");
170
171        let mut args = vec![
172            "cuda",
173            "build",
174            "--shader-crate",
175            self.shader_crate
176                .to_str()
177                .expect("Invalid shader crate path"),
178            "--output-dir",
179            output_dir.to_str().expect("Invalid output directory path"),
180        ];
181
182        if !features_str.is_empty() {
183            args.push("--features");
184            args.push(&features_str);
185        }
186
187        let status = Command::new("cargo")
188            .args(args)
189            .env("RUST_MIN_STACK", self.rust_min_stack.to_string())
190            .status()
191            .expect("failed to run cargo cuda");
192
193        if !status.success() {
194            panic!("cargo cuda build failed");
195        }
196    }
197}