Skip to main content

concinnity_toolchain/
lib.rs

1//! Shared build-script support for the workspace.
2//!
3//! Besides the Metal shader precompilation in `metal_shaders` and the source
4//! hashing in `source_hash`, two responsibilities, both previously copy-pasted
5//! between the runtime crate's build script and the editor crate's build script
6//! (and missing entirely from the example binaries, which is why they failed to
7//! link against the runtime's DLSS code on Windows):
8//!
9//! 1. Resolve the rendering backend once and emit it as a single cfg
10//!    (`backend_metal` / `backend_dx` / `backend_vk`) the source gates on.
11//!
12//! 2. Detect the optional graphics SDKs and emit the cfgs the renderer gates on
13//!    (`agility_sdk_configured`, `ffx_sdk_bundled`, `xess_sdk_bundled`,
14//!    `ngx_sdk_bundled`, `dxc_bundled`). For a package that produces final
15//!    binaries this also copies the runtime DLLs next to the .exe and links the
16//!    NGX import lib; for a package that produces only an rlib and its own test
17//!    binaries just the NGX link is needed. Which of the two, and where the
18//!    binaries land, is read off the calling package by `targets` -- a build
19//!    script declares nothing about its own target list.
20//!
21//! The public entry points emit `cargo::` directives on stdout, which Cargo
22//! attributes to the build script of whichever package called in. That is what
23//! lets an example binary's build script pick up the same NGX link and DLL
24//! bundling the CLI's does, without duplicating any of this logic.
25//!
26//! This file is the thin environment-reading layer: it snapshots everything the
27//! setup needs from the process environment into an `SdkEnv` and prints the
28//! directives. The probe/copy/directive logic itself lives in the `sdks`
29//! module, which never touches the environment or stdout.
30
31use std::path::{Path, PathBuf};
32
33mod metal_shaders;
34mod sdks;
35mod slang_artifacts;
36mod source_hash;
37mod targets;
38
39pub use metal_shaders::{SlangLibSpec, SlangShaders, precompile_metal_shaders};
40use sdks::SdkEnv;
41pub use slang_artifacts::{SlangArtifact, precompile_slang_artifacts, watch_shader_dir};
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44/// The graphics backend a build targets.
45pub enum Backend {
46    /// Metal, on macOS.
47    Metal,
48    /// DirectX 12, on Windows.
49    Dx,
50    /// Vulkan, on Windows and Linux.
51    Vk,
52}
53
54impl Backend {
55    pub(crate) fn cfg_name(self) -> &'static str {
56        match self {
57            Backend::Metal => "backend_metal",
58            Backend::Dx => "backend_dx",
59            Backend::Vk => "backend_vk",
60        }
61    }
62}
63
64// Which of the calling package's targets are the final binaries the graphics
65// SDKs serve. Cargo scopes a linker argument by target kind and places each kind
66// in its own directory, so this picks both the `cargo::rustc-link-arg-*` key the
67// Agility exports go out under and the directory the runtime DLLs are copied
68// into -- which has to be the one holding the .exe, since that is where Windows
69// looks for them. Discovered per package by `targets`, never named by a caller.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub(crate) enum BinaryTargets {
72    // The package links no binary of its own. Its test and bench executables
73    // still resolve the NGX symbols through the plain `rustc-link-arg`, but
74    // nothing is placed beside them.
75    None,
76    // `src/main.rs` and `src/bin/`, which land in `<target>/<profile>/`.
77    Bins,
78    // `examples/`, which land in `<target>/<profile>/examples/`.
79    Examples,
80}
81
82impl BinaryTargets {
83    pub(crate) fn bundles(self) -> bool {
84        self != BinaryTargets::None
85    }
86
87    // The `cargo::rustc-link-arg-*` key covering these targets. Cargo rejects
88    // `rustc-link-arg-bins` outright from a package with no bin target, and has
89    // no per-example form at all, so an argument emitted for `Examples` reaches
90    // every example the package builds.
91    pub(crate) fn link_arg_key(self) -> Option<&'static str> {
92        match self {
93            BinaryTargets::None => None,
94            BinaryTargets::Bins => Some("cargo::rustc-link-arg-bins"),
95            BinaryTargets::Examples => Some("cargo::rustc-link-arg-examples"),
96        }
97    }
98
99    // Subdirectory of `<target>/<profile>/` Cargo writes these binaries to.
100    pub(crate) fn exe_subdir(self) -> Option<&'static str> {
101        matches!(self, BinaryTargets::Examples).then_some("examples")
102    }
103}
104
105// Resolve the backend from the target OS and whether the `vulkan` feature is on.
106// macOS defaults to Metal and Windows to DirectX; both opt into Vulkan with the
107// feature. Everything else (Linux) is Vulkan regardless. macOS Vulkan runs over
108// MoltenVK and exists for cross-backend testing, not for shipping.
109pub(crate) fn resolve_backend(target_os: &str, vulkan: bool) -> Backend {
110    match (target_os, vulkan) {
111        ("macos", false) => Backend::Metal,
112        ("windows", false) => Backend::Dx,
113        _ => Backend::Vk,
114    }
115}
116
117/// Declare every cfg the renderer source gates on so `--check-cfg` does not warn.
118/// A package only needs this if its own source references one of these cfgs.
119pub fn emit_check_cfgs() {
120    for line in sdks::check_cfg_directives() {
121        println!("{line}");
122    }
123}
124
125/// Resolve the backend from the Cargo-provided environment, emitting nothing.
126/// For a package that needs the backend only to pick its SDK setup and never
127/// gates its own source on one, so has no reason to carry the cfg.
128pub fn backend_from_cargo() -> Backend {
129    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
130    let vulkan = std::env::var("CARGO_FEATURE_VULKAN").is_ok();
131    resolve_backend(&target_os, vulkan)
132}
133
134/// Resolve the backend from the Cargo-provided environment and emit the
135/// `rustc-cfg` for it, returning the choice so the caller can branch.
136pub fn emit_backend_cfg() -> Backend {
137    let backend = backend_from_cargo();
138    println!("{}", sdks::backend_cfg_directive(backend));
139    backend
140}
141
142/// Set up the optional graphics SDKs for the given backend. On a non-Windows
143/// target (or the Metal backend) this is a no-op: none of these SDKs apply.
144///
145/// Which kinds of final binary the calling package builds is read from that
146/// package, not passed in: a package can build both bins and examples, and each
147/// kind takes its own linker-argument key and its own directory for the bundled
148/// DLLs, so the setup runs once per kind. A directive both kinds produce --
149/// every cfg, every warning -- is emitted once.
150pub fn setup_graphics_sdks(backend: Backend) {
151    let env = sdk_env_from_cargo();
152    if let Some(dir) = manifest_dir() {
153        for path in targets::watched_inputs(&dir) {
154            println!("cargo::rerun-if-changed={}", path.display());
155        }
156    }
157    for line in sdks::graphics_sdk_directives(backend, &binary_targets_from_cargo(), &env) {
158        println!("{line}");
159    }
160}
161
162// The calling package's binary kinds, from the manifest Cargo is building it
163// from. A package Cargo hands no manifest (nothing does outside a build script)
164// falls back to the kind that scopes nothing.
165fn binary_targets_from_cargo() -> Vec<BinaryTargets> {
166    let Some(dir) = manifest_dir() else {
167        return vec![BinaryTargets::None];
168    };
169    let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap_or_default();
170    targets::binary_targets(&manifest, &dir)
171}
172
173fn manifest_dir() -> Option<PathBuf> {
174    std::env::var("CARGO_MANIFEST_DIR").ok().map(PathBuf::from)
175}
176
177/// Hash the Rust sources under `roots`, and emit the rerun directives that
178/// re-run the calling build script when any of them change. Each root is either
179/// a directory tree (every `.rs` under it participates) or a single file.
180///
181/// The hash is what a content-addressed cache folds in so that a change to the
182/// code producing its stored bytes evicts entries whose other inputs did not
183/// move. See `source_hash` for the shape of the guarantee.
184pub fn hash_sources(roots: &[PathBuf]) -> u32 {
185    let package =
186        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("build script runs under cargo"));
187    let package = package.canonicalize().unwrap_or(package);
188    let mut named = Vec::new();
189    for root in roots {
190        // Directory-level rerun directives catch added and removed files.
191        println!("cargo:rerun-if-changed={}", root.display());
192        let mut files = Vec::new();
193        source_hash::collect(root, &mut files);
194        named.extend(
195            files
196                .into_iter()
197                .map(|file| (source_hash::relative_name(&package, &file), file)),
198        );
199    }
200    source_hash::hash_named(&mut named)
201}
202
203// Default SDK install roots, used when the matching env var is unset.
204const DEFAULT_AGILITY_SDK_ROOT: &str = "C:\\microsoft.direct3d.d3d12.1.619.3";
205const DEFAULT_FIDELITYFX_SDK_ROOT: &str = "C:\\FidelityFX-SDK-v1.1.4";
206const DEFAULT_XESS_SDK_ROOT: &str = "C:\\XeSS_SDK_3.0.1";
207const DEFAULT_STREAMLINE_SDK_ROOT: &str = "C:\\streamline-sdk-v2.11.1";
208const DEFAULT_WINDOWS_SDK_BIN: &str = "C:\\Program Files (x86)\\Windows Kits\\10\\bin";
209
210// Whether an opt-in variable's value asks for the feature, for the one SDK that
211// is off by default. Bundling Agility links `D3D12SDKVersion` / `D3D12SDKPath`
212// into the binary, and `d3d12.dll` reads those before any engine code runs: a
213// binary carrying them starts only where the staged `D3D12/` directory sits
214// beside it, so an executable copied anywhere else -- every `cargo install` --
215// reaches no adapter at all. That makes bundling a decision about how the
216// artifact is distributed rather than about which SDKs the build machine
217// happens to have, so it has to be asked for. `0` keeps meaning off, as it
218// always has for these variables.
219fn opted_in(value: Option<&str>) -> bool {
220    matches!(value, Some("1" | "true" | "TRUE"))
221}
222
223// Snapshot every environment input the SDK setup reads. This is the only place
224// the setup consults the process environment; everything downstream works on
225// the returned struct.
226fn sdk_env_from_cargo() -> SdkEnv {
227    let var = |name: &str| std::env::var(name).ok();
228    let root = |name: &str, default: &str| {
229        var(name)
230            .map(PathBuf::from)
231            .unwrap_or_else(|| PathBuf::from(default))
232    };
233    // The `LoadLibrary`-at-runtime SDKs default to ON and are opted out of with
234    // `<VAR>=0`: an absent DLL costs the feature and nothing else.
235    let enabled = |name: &str| var(name).as_deref() != Some("0");
236    // Agility is the exception and defaults to OFF; see `opted_in`.
237    let opted_in = |name: &str| self::opted_in(var(name).as_deref());
238    SdkEnv {
239        target_os: var("CARGO_CFG_TARGET_OS").unwrap_or_default(),
240        out_dir: var("OUT_DIR").map(PathBuf::from),
241        workspace_root: workspace_root(),
242        agility_root: root("AGILITY_SDK_ROOT", DEFAULT_AGILITY_SDK_ROOT),
243        fidelityfx_root: root("FIDELITYFX_SDK_ROOT", DEFAULT_FIDELITYFX_SDK_ROOT),
244        xess_root: root("XESS_SDK_ROOT", DEFAULT_XESS_SDK_ROOT),
245        streamline_root: root("STREAMLINE_SDK_ROOT", DEFAULT_STREAMLINE_SDK_ROOT),
246        dxc_root: var("DXC_SDK_ROOT").map(PathBuf::from),
247        windows_sdk_bin: PathBuf::from(DEFAULT_WINDOWS_SDK_BIN),
248        agility_enabled: opted_in("CN_ENABLE_AGILITY_SDK"),
249        ffx_enabled: enabled("CN_ENABLE_FFX_FSR3"),
250        xess_enabled: enabled("CN_ENABLE_XESS"),
251        dlss_enabled: enabled("CN_ENABLE_DLSS"),
252        dxc_enabled: enabled("CN_ENABLE_DXC"),
253    }
254}
255
256// Locate the workspace root by walking up from the caller's manifest until a
257// `Cargo.toml` declaring `[workspace]` is found.
258fn workspace_root() -> Option<PathBuf> {
259    let start = std::env::var("CARGO_MANIFEST_DIR").ok()?;
260    find_ancestor_with(Path::new(&start), |dir| {
261        std::fs::read_to_string(dir.join("Cargo.toml"))
262            .map(|c| c.contains("[workspace]"))
263            .unwrap_or(false)
264    })
265}
266
267// Walk `start` and its ancestors, returning the first that satisfies `pred`.
268fn find_ancestor_with(start: &Path, pred: impl Fn(&Path) -> bool) -> Option<PathBuf> {
269    let mut dir = start.to_path_buf();
270    loop {
271        if pred(&dir) {
272            return Some(dir);
273        }
274        if !dir.pop() {
275            return None;
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn backend_resolution_covers_every_target() {
286        assert_eq!(resolve_backend("macos", false), Backend::Metal);
287        assert_eq!(resolve_backend("macos", true), Backend::Vk);
288        assert_eq!(resolve_backend("windows", false), Backend::Dx);
289        assert_eq!(resolve_backend("windows", true), Backend::Vk);
290        assert_eq!(resolve_backend("linux", false), Backend::Vk);
291        assert_eq!(resolve_backend("linux", true), Backend::Vk);
292    }
293
294    #[test]
295    fn backend_cfg_names_are_stable() {
296        assert_eq!(Backend::Metal.cfg_name(), "backend_metal");
297        assert_eq!(Backend::Dx.cfg_name(), "backend_dx");
298        assert_eq!(Backend::Vk.cfg_name(), "backend_vk");
299    }
300
301    #[test]
302    fn ancestor_search_finds_marked_dir() {
303        let start = Path::new("/a/b/c/d");
304        let hit = find_ancestor_with(start, |p| p == Path::new("/a/b"));
305        assert_eq!(hit, Some(PathBuf::from("/a/b")));
306
307        let miss = find_ancestor_with(start, |p| p == Path::new("/x"));
308        assert_eq!(miss, None);
309    }
310
311    #[test]
312    fn workspace_root_finds_the_workspace_manifest() {
313        // Cargo sets CARGO_MANIFEST_DIR for test binaries, so the walk starts
314        // at this crate and must land on the workspace's own Cargo.toml.
315        let root = workspace_root().expect("workspace root");
316        let manifest = std::fs::read_to_string(root.join("Cargo.toml")).expect("read manifest");
317        assert!(manifest.contains("[workspace]"));
318    }
319
320    #[test]
321    fn sdk_env_snapshot_defaults_the_loadlibrary_probes_on() {
322        let env = sdk_env_from_cargo();
323        // The `LoadLibrary` probes default ON when their opt-out variable is
324        // unset. Only assert for variables the surrounding environment leaves
325        // unset, so a local `<VAR>=0` opt-out does not fail the test.
326        for (var, flag) in [
327            ("CN_ENABLE_FFX_FSR3", env.ffx_enabled),
328            ("CN_ENABLE_XESS", env.xess_enabled),
329            ("CN_ENABLE_DLSS", env.dlss_enabled),
330            ("CN_ENABLE_DXC", env.dxc_enabled),
331        ] {
332            if std::env::var(var).is_err() {
333                assert!(flag, "{var} should default on");
334            }
335        }
336        // Roots fall back to the hardcoded defaults when unset.
337        if std::env::var("XESS_SDK_ROOT").is_err() {
338            assert_eq!(env.xess_root, PathBuf::from(DEFAULT_XESS_SDK_ROOT));
339        }
340    }
341
342    // Agility is the one that binds the executable to a directory beside it, so
343    // an unset variable must leave it OFF: the default has to be the artifact
344    // that runs anywhere, not the one that runs only where it was built.
345    #[test]
346    fn sdk_env_snapshot_defaults_agility_off() {
347        if std::env::var("CN_ENABLE_AGILITY_SDK").is_err() {
348            assert!(!sdk_env_from_cargo().agility_enabled);
349        }
350    }
351
352    // Only an affirmative value opts in. `0` has always meant off and keeps
353    // meaning off, so an environment carrying the old opt-out is unaffected.
354    #[test]
355    fn only_an_affirmative_value_opts_into_agility() {
356        for on in ["1", "true", "TRUE"] {
357            assert!(opted_in(Some(on)), "{on}");
358        }
359        for off in ["0", "", "no", "yes", "2", "false"] {
360            assert!(!opted_in(Some(off)), "{off}");
361        }
362        assert!(!opted_in(None));
363    }
364
365    #[test]
366    fn graphics_sdk_setup_is_a_noop_off_windows_targets() {
367        // Metal never has SDKs to set up, and the Vulkan arm is gated on a
368        // Windows target OS (CARGO_CFG_TARGET_OS is unset outside build
369        // scripts), so neither requires any SDK to be present.
370        let env = sdk_env_from_cargo();
371        for targets in [
372            &[BinaryTargets::None][..],
373            &[BinaryTargets::Bins],
374            &[BinaryTargets::Examples],
375            &[BinaryTargets::Bins, BinaryTargets::Examples],
376        ] {
377            for backend in [Backend::Metal, Backend::Vk] {
378                assert!(sdks::graphics_sdk_directives(backend, targets, &env).is_empty());
379            }
380        }
381        setup_graphics_sdks(Backend::Metal);
382        setup_graphics_sdks(Backend::Vk);
383        // The check-cfg list is emitted unconditionally and must not panic.
384        emit_check_cfgs();
385    }
386
387    #[test]
388    fn this_crate_builds_no_final_binary() {
389        // concinnity-toolchain is a lib with no bin and no example, so the
390        // discovery run from its own test binary has to say so.
391        assert_eq!(binary_targets_from_cargo(), vec![BinaryTargets::None]);
392    }
393}