use concinnity_core::blob::CacheEntryKind;
#[cfg(any(backend_dx, backend_vk))]
use concinnity_host::store::cache::Segment;
use sha2::{Digest, Sha256};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
include!(concat!(env!("OUT_DIR"), "/shader_compile_source_hash.rs"));
const KIND: CacheEntryKind = CacheEntryKind::Shader;
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(
bundle: &mut Segment,
key: &Key<'_>,
compile: impl FnOnce() -> Result<Vec<u8>, String>,
) -> Result<Ensured, String> {
if enabled() {
verify_toolchain();
}
let digest = key.digest();
if bundle.get(KIND, &digest).is_some_and(|b| !b.is_empty()) {
return Ok(Ensured::Present);
}
if enabled()
&& let Some(bytes) = load(&digest)
{
bundle.put(KIND, &digest, &bytes);
return Ok(Ensured::Copied);
}
let bytes = compile()?;
if bytes.is_empty() {
return Err("compile produced an empty artifact".to_string());
}
bundle.put(KIND, &digest, &bytes);
if enabled() {
store(&digest, &bytes);
}
Ok(Ensured::Compiled)
}
pub(crate) fn report_init() {
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
);
}
fn enabled() -> bool {
crate::runtime_cache::enabled()
}
fn verify_toolchain() {
static ONCE: OnceLock<()> = OnceLock::new();
ONCE.get_or_init(|| {
let current = concinnity_slang::compiler_id();
if crate::runtime_cache::verify_toolchain(current) {
tracing::info!("shader cache: {current} did not write it, discarding entries");
}
});
}
fn load(digest: &str) -> Option<Vec<u8>> {
let usable = |bytes: Vec<u8>| (!bytes.is_empty()).then_some(bytes);
crate::runtime_cache::load(KIND, digest)
.and_then(usable)
.or_else(|| crate::runtime_cache::load_bundled(KIND, digest).and_then(usable))
}
fn store(digest: &str, bytes: &[u8]) {
crate::runtime_cache::store(KIND, digest, bytes);
}
#[cfg(test)]
mod tests {
use super::*;
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 the_cache_is_off_under_test() {
assert!(!enabled());
assert_eq!(load("deadbeef"), None);
}
#[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 path = concinnity_host::store::paths::runtime_cache_in(&dir);
let mut bundle = Segment::read_from(&path);
let k = key("ensure src", "main", "ps_5_1", 3);
let first = ensure_in(&mut bundle, &k, || Ok(vec![7, 7, 7])).unwrap();
assert_eq!(first, Ensured::Compiled);
let second = ensure_in(&mut bundle, &k, || panic!("must not recompile")).unwrap();
assert_eq!(second, Ensured::Present);
assert!(!path.exists(), "warming is memory until the caller writes");
assert!(bundle.write_to(&path, 1 << 20));
let mut shipped = Segment::read_from(&path);
assert_eq!(shipped.get(KIND, &k.digest()), Some(&[7, 7, 7][..]));
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 mut bundle = Segment::read_from(std::path::Path::new("/nonexistent/cache/0"));
let k = key("bad src", "main", "ps_5_1", 0);
assert!(ensure_in(&mut bundle, &k, || Err("boom".to_string())).is_err());
assert!(
ensure_in(&mut bundle, &k, || Ok(Vec::new())).is_err(),
"empty"
);
assert_eq!(bundle.get(KIND, &k.digest()), None);
}
}