use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
include!(concat!(env!("OUT_DIR"), "/shader_compile_source_hash.rs"));
const CACHE_BUDGET_BYTES: u64 = 64 * 1024 * 1024;
pub(crate) struct Key<'a> {
pub compiler: &'a str,
pub source: &'a str,
pub entry: &'a str,
pub target: &'a str,
pub options: u64,
}
impl Key<'_> {
fn digest(&self) -> String {
let mut h = Sha256::new();
h.update(SHADER_COMPILE_SOURCE_HASH.to_le_bytes());
h.update(concinnity_slang::SOURCE_HASH.to_le_bytes());
for part in [self.compiler, self.source, self.entry, self.target] {
h.update((part.len() as u64).to_le_bytes());
h.update(part.as_bytes());
}
h.update(self.options.to_le_bytes());
format!("{:x}", h.finalize())
}
}
static HITS: AtomicU64 = AtomicU64::new(0);
static MISSES: AtomicU64 = AtomicU64::new(0);
static COMPILE_MICROS: AtomicU64 = AtomicU64::new(0);
pub(crate) fn cached(
key: &Key<'_>,
label: &str,
compile: impl FnOnce() -> Result<Vec<u8>, String>,
) -> Result<Vec<u8>, String> {
if !enabled() {
return compile();
}
verify_toolchain();
let digest = key.digest();
if let Some(bytes) = load(&digest) {
HITS.fetch_add(1, Ordering::Relaxed);
return Ok(bytes);
}
let started = std::time::Instant::now();
let bytes = compile()?;
let micros = started.elapsed().as_micros() as u64;
MISSES.fetch_add(1, Ordering::Relaxed);
COMPILE_MICROS.fetch_add(micros, Ordering::Relaxed);
tracing::debug!(
"shader cache miss: {} {} ({:.1} ms)",
key.entry,
label,
micros as f64 / 1000.0
);
store(&digest, &bytes);
Ok(bytes)
}
#[cfg(any(backend_dx, backend_vk))]
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Ensured {
Present,
Copied,
Compiled,
}
#[cfg(any(backend_dx, backend_vk))]
pub(crate) fn ensure_in(
dir: &Path,
key: &Key<'_>,
compile: impl FnOnce() -> Result<Vec<u8>, String>,
) -> Result<Ensured, String> {
if enabled() {
verify_toolchain();
}
let digest = key.digest();
if load_in(dir, &digest).is_some() {
return Ok(Ensured::Present);
}
if enabled()
&& let Some(bytes) = load(&digest)
{
store_in(dir, &digest, &bytes);
return Ok(Ensured::Copied);
}
let bytes = compile()?;
if bytes.is_empty() {
return Err("compile produced an empty artifact".to_string());
}
store_in(dir, &digest, &bytes);
if enabled() {
store(&digest, &bytes);
}
Ok(Ensured::Compiled)
}
pub(crate) fn report_init_and_prune() {
let (hits, misses, micros) = (
HITS.load(Ordering::Relaxed),
MISSES.load(Ordering::Relaxed),
COMPILE_MICROS.load(Ordering::Relaxed),
);
if hits + misses == 0 {
return;
}
tracing::info!(
"shader cache: {hits} reused, {misses} compiled ({:.0} ms) at renderer init",
micros as f64 / 1000.0
);
if misses > 0
&& let Some(dir) = cache_dir()
{
prune(&dir, CACHE_BUDGET_BYTES);
}
}
fn enabled() -> bool {
!cfg!(test)
}
fn cache_dir() -> Option<PathBuf> {
concinnity_host::store::paths::shader_cache_dir()
}
pub(crate) fn slang_work_dir() -> PathBuf {
cache_dir().map_or_else(
|| std::env::temp_dir().join("concinnity-slang-work"),
|dir| dir.join("slang-work"),
)
}
const TOOLCHAIN_STAMP: &str = "toolchain";
fn verify_toolchain() {
static ONCE: OnceLock<()> = OnceLock::new();
ONCE.get_or_init(|| {
let Some(dir) = cache_dir() else {
return;
};
let current = concinnity_slang::compiler_id();
let stamp = dir.join(TOOLCHAIN_STAMP);
if std::fs::read_to_string(&stamp).is_ok_and(|found| found == current) {
return;
}
discard_entries(&dir);
if std::fs::create_dir_all(&dir).is_ok() {
let _ = std::fs::write(&stamp, current);
}
});
}
fn discard_entries(dir: &Path) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
if entry.metadata().is_ok_and(|meta| meta.is_file()) {
let _ = std::fs::remove_file(entry.path());
}
}
}
fn load(digest: &str) -> Option<Vec<u8>> {
dirs_to_read(
cache_dir(),
concinnity_host::store::paths::bundled_shader_cache_dir(),
)
.iter()
.find_map(|dir| load_in(dir, digest))
}
fn dirs_to_read(writable: Option<PathBuf>, bundled: Option<PathBuf>) -> Vec<PathBuf> {
match (writable, bundled) {
(Some(w), Some(b)) if w == b => vec![w],
(Some(w), Some(b)) => vec![w, b],
(Some(d), None) | (None, Some(d)) => vec![d],
(None, None) => Vec::new(),
}
}
fn load_in(dir: &Path, digest: &str) -> Option<Vec<u8>> {
let bytes = std::fs::read(dir.join(digest)).ok()?;
(!bytes.is_empty()).then_some(bytes)
}
fn store(digest: &str, bytes: &[u8]) {
let Some(dir) = cache_dir() else {
return;
};
store_in(&dir, digest, bytes);
}
fn store_in(dir: &Path, digest: &str, bytes: &[u8]) {
if std::fs::create_dir_all(dir).is_err() {
return;
}
let tmp = dir.join(format!("{digest}.{}.tmp", std::process::id()));
if std::fs::write(&tmp, bytes).is_ok() && std::fs::rename(&tmp, dir.join(digest)).is_err() {
let _ = std::fs::remove_file(&tmp);
}
}
fn prune(dir: &Path, budget: u64) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
let mut listing: Vec<(std::time::SystemTime, u64, PathBuf)> = entries
.flatten()
.filter_map(|e| {
let meta = e.metadata().ok()?;
if !meta.is_file() || e.file_name() == TOOLCHAIN_STAMP {
return None;
}
Some((meta.modified().ok()?, meta.len(), e.path()))
})
.collect();
for path in evictions(&mut listing, budget) {
let _ = std::fs::remove_file(path);
}
}
fn evictions(
listing: &mut [(std::time::SystemTime, u64, PathBuf)],
budget: u64,
) -> Vec<std::path::PathBuf> {
let mut total: u64 = listing.iter().map(|(_, len, _)| len).sum();
if total <= budget {
return Vec::new();
}
listing.sort_by_key(|(modified, _, _)| *modified);
let mut doomed = Vec::new();
for (_, len, path) in listing.iter() {
if total <= budget {
break;
}
total -= len;
doomed.push(path.clone());
}
doomed
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, SystemTime};
fn key<'a>(source: &'a str, entry: &'a str, target: &'a str, options: u64) -> Key<'a> {
Key {
compiler: "fxc",
source,
entry,
target,
options,
}
}
#[test]
fn digest_is_stable_for_identical_inputs() {
let a = key("float4 main() { return 0; }", "main", "ps_5_1", 7);
let b = key("float4 main() { return 0; }", "main", "ps_5_1", 7);
assert_eq!(a.digest(), b.digest());
}
#[test]
fn every_field_changes_the_digest() {
let base = key("src", "main", "ps_5_1", 1).digest();
assert_ne!(base, key("other", "main", "ps_5_1", 1).digest(), "source");
assert_ne!(base, key("src", "main2", "ps_5_1", 1).digest(), "entry");
assert_ne!(base, key("src", "main", "vs_5_1", 1).digest(), "target");
assert_ne!(base, key("src", "main", "ps_5_1", 2).digest(), "options");
let mut other_compiler = key("src", "main", "ps_5_1", 1);
other_compiler.compiler = "glsl";
assert_ne!(base, other_compiler.digest(), "compiler");
}
#[test]
fn field_boundaries_cannot_be_confused() {
assert_ne!(
key("ab", "c", "t", 0).digest(),
key("a", "bc", "t", 0).digest()
);
}
#[test]
fn store_then_load_round_trips_through_a_directory() {
let dir = std::env::temp_dir().join(format!("cn_shader_cache_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
store_in(&dir, "deadbeef", &[1, 2, 3, 4]);
assert_eq!(
std::fs::read(dir.join("deadbeef")).ok(),
Some(vec![1, 2, 3, 4])
);
let leftovers = std::fs::read_dir(&dir)
.unwrap()
.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "tmp"))
.count();
assert_eq!(leftovers, 0);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn discarding_entries_spares_subdirectories() {
let tmp = std::env::temp_dir().join(format!("cn_sc_discard_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
store_in(&tmp, "cafe", &[1, 2]);
store_in(&tmp, "f00d", &[3, 4]);
let work = tmp.join("slang-work");
std::fs::create_dir_all(&work).unwrap();
std::fs::write(work.join("in-flight.slang"), "x").unwrap();
discard_entries(&tmp);
assert!(load_in(&tmp, "cafe").is_none());
assert!(load_in(&tmp, "f00d").is_none());
assert!(work.join("in-flight.slang").exists());
std::fs::remove_dir_all(&tmp).unwrap();
}
#[test]
fn pruning_never_evicts_the_toolchain_stamp() {
let tmp = std::env::temp_dir().join(format!("cn_sc_stamp_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join(TOOLCHAIN_STAMP), "slang 2026.1").unwrap();
store_in(&tmp, "cafe", &[0u8; 512]);
prune(&tmp, 16);
assert!(tmp.join(TOOLCHAIN_STAMP).exists());
assert!(load_in(&tmp, "cafe").is_none(), "artifact should evict");
std::fs::remove_dir_all(&tmp).unwrap();
}
#[test]
fn a_shared_path_is_searched_once() {
let p = PathBuf::from("/state/shader-cache");
assert_eq!(dirs_to_read(Some(p.clone()), Some(p.clone())), vec![p]);
}
#[test]
fn a_read_only_install_searches_writable_then_bundled() {
let writable = PathBuf::from("/user/appdata/shader-cache");
let bundled = PathBuf::from("/program files/game/shader-cache");
assert_eq!(
dirs_to_read(Some(writable.clone()), Some(bundled.clone())),
vec![writable, bundled]
);
}
#[test]
fn no_state_root_searches_nothing() {
assert!(dirs_to_read(None, None).is_empty());
}
#[test]
fn an_artifact_is_found_in_the_bundled_tier() {
let tmp = std::env::temp_dir().join(format!("cn_sc_tiers_{}", std::process::id()));
let writable = tmp.join("writable");
let bundled = tmp.join("bundled");
let _ = std::fs::remove_dir_all(&tmp);
store_in(&bundled, "cafe", &[9, 9]);
let dirs = dirs_to_read(Some(writable.clone()), Some(bundled.clone()));
assert_eq!(
dirs.iter().find_map(|d| load_in(d, "cafe")),
Some(vec![9, 9])
);
store_in(&writable, "cafe", &[1, 1]);
assert_eq!(
dirs.iter().find_map(|d| load_in(d, "cafe")),
Some(vec![1, 1])
);
std::fs::remove_dir_all(&tmp).unwrap();
}
#[test]
fn a_truncated_artifact_reads_as_a_miss() {
let tmp = std::env::temp_dir().join(format!("cn_sc_trunc_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
store_in(&tmp, "empty", &[]);
assert_eq!(load_in(&tmp, "empty"), None);
std::fs::remove_dir_all(&tmp).unwrap();
}
fn entry(secs: u64, len: u64, name: &str) -> (SystemTime, u64, PathBuf) {
(
SystemTime::UNIX_EPOCH + Duration::from_secs(secs),
len,
PathBuf::from(name),
)
}
#[test]
fn nothing_is_evicted_under_budget() {
let mut listing = [entry(1, 10, "a"), entry(2, 10, "b")];
assert!(evictions(&mut listing, 100).is_empty());
}
#[test]
fn eviction_drops_oldest_first_until_it_fits() {
let mut listing = [
entry(3, 40, "newest"),
entry(1, 40, "oldest"),
entry(2, 40, "middle"),
];
let doomed = evictions(&mut listing, 80);
assert_eq!(doomed, [PathBuf::from("oldest")]);
}
#[test]
fn eviction_can_clear_everything_for_a_zero_budget() {
let mut listing = [entry(1, 40, "a"), entry(2, 40, "b")];
assert_eq!(evictions(&mut listing, 0).len(), 2);
}
#[cfg(any(backend_dx, backend_vk))]
#[test]
fn ensure_in_compiles_once_then_finds_the_artifact_present() {
let dir = std::env::temp_dir().join(format!("cn_sc_ensure_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let k = key("ensure src", "main", "ps_5_1", 3);
let first = ensure_in(&dir, &k, || Ok(vec![7, 7, 7])).unwrap();
assert_eq!(first, Ensured::Compiled);
assert_eq!(load_in(&dir, &k.digest()), Some(vec![7, 7, 7]));
let second = ensure_in(&dir, &k, || panic!("must not recompile")).unwrap();
assert_eq!(second, Ensured::Present);
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(any(backend_dx, backend_vk))]
#[test]
fn ensure_in_propagates_a_compile_error_and_stores_nothing() {
let dir = std::env::temp_dir().join(format!("cn_sc_ensure_err_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let k = key("bad src", "main", "ps_5_1", 0);
assert!(ensure_in(&dir, &k, || Err("boom".to_string())).is_err());
assert!(ensure_in(&dir, &k, || Ok(Vec::new())).is_err(), "empty");
assert_eq!(load_in(&dir, &k.digest()), None);
let _ = std::fs::remove_dir_all(&dir);
}
}