Skip to main content

concinnity_device/
precompile.rs

1//! Export-time compilation of the engine's built-in shaders. The DirectX and
2//! Vulkan backends declare their compile set as static data (each backend's
3//! builtins.rs); this module iterates those declarations and makes sure every
4//! enumerable variant's artifact is in the runtime cache segment a bundle
5//! ships. Compilation is pure CPU (slangc needs no GPU device), so
6//! this runs inside `cn export` with no window, no adapter, and no child
7//! process. Renderer init compiles through the same declarations and the same
8//! cache keys, so a shipped bundle's first launch reuses every artifact written
9//! here; anything not enumerable (a world-authored SdfVolume fragment, an
10//! unusual runtime parameter) still compiles at init exactly as before.
11//!
12//! The segment is built in memory and written once when the run finishes, so
13//! warming a hundred artifacts costs one file write rather than a hundred.
14
15use std::path::Path;
16
17use concinnity_host::store::cache::{CACHE_BUDGET_BYTES, Segment};
18use concinnity_host::store::paths::StateTree;
19
20use crate::shader_cache::Ensured;
21
22/// Outcome of a built-in shader precompile: how many artifacts were already in
23/// place or copied from the local cache, how many compiled fresh, and the
24/// programs that failed (with their compile diagnostics). Failures do not
25/// abort the run -- the affected shader falls back to compiling at the
26/// bundle's first launch.
27#[derive(Debug, Default)]
28pub struct Report {
29    /// Artifacts already in place or copied from the local cache.
30    pub reused: usize,
31    /// Artifacts compiled fresh this run.
32    pub compiled: usize,
33    /// Programs that failed, with their compile diagnostics.
34    pub failed: Vec<String>,
35}
36
37impl Report {
38    pub(crate) fn record(&mut self, label: &str, result: Result<Ensured, String>) {
39        match result {
40            Ok(Ensured::Present) | Ok(Ensured::Copied) => self.reused += 1,
41            Ok(Ensured::Compiled) => self.compiled += 1,
42            Err(e) => self.failed.push(format!("{label}: {e}")),
43        }
44    }
45
46    /// Total artifacts now in the bundle's segment.
47    pub fn cached(&self) -> usize {
48        self.reused + self.compiled
49    }
50}
51
52/// Compile every enumerable built-in shader variant into the runtime cache
53/// segment under `state_dir`, which is a bundle's state root.
54///
55/// Nothing about the exported world enters this: every variant a backend can
56/// take is a property of the device the bundle eventually runs on -- its MSAA
57/// mode, its probe cube-array length, whether it seats the bindless pool at its
58/// ceiling -- so each is baked at what a desktop driver affords, and a device
59/// that differs misses those entries and compiles at first launch.
60///
61/// The segment is left unstamped by the host toolchain that warmed it: a
62/// player's own slangc is whatever it is, and a shipped artifact is a function
63/// of its source rather than of what compiled it.
64pub fn precompile_builtin_shaders(state_dir: &Path) -> Report {
65    let path = StateTree::at(state_dir).bundled_runtime_cache_path();
66    let mut bundle = Segment::read_from(&path);
67    let mut report = Report::default();
68    #[cfg(backend_dx)]
69    crate::directx::slang_builtins::precompile(&mut bundle, &mut report);
70    #[cfg(backend_vk)]
71    crate::vulkan::builtins::precompile(&mut bundle, &mut report);
72    bundle.write_to(&path, CACHE_BUDGET_BYTES);
73    // `ensure_in` also keeps a copy in this machine's own cache segment, so
74    // repeated exports stay warm. That copy is memory until a checkpoint.
75    crate::runtime_cache::checkpoint();
76    report
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn report_tallies_reuse_compile_and_failure() {
85        let mut r = Report::default();
86        r.record("a", Ok(Ensured::Present));
87        r.record("b", Ok(Ensured::Copied));
88        r.record("c", Ok(Ensured::Compiled));
89        r.record("d ps_5_1", Err("boom".to_string()));
90        assert_eq!(r.reused, 2);
91        assert_eq!(r.compiled, 1);
92        assert_eq!(r.cached(), 3);
93        assert_eq!(r.failed, vec!["d ps_5_1: boom".to_string()]);
94    }
95}