use concinnity_core::blob::CacheEntryKind;
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,
}
impl Key<'_> {
fn digest(&self) -> String {
let mut h = Sha256::new();
h.update(SHADER_COMPILE_SOURCE_HASH.to_le_bytes());
h.update(concinnity_shader::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());
}
hex::encode(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<E>(
key: &Key<'_>,
label: &str,
compile: impl FnOnce() -> Result<Vec<u8>, E>,
) -> Result<Vec<u8>, E> {
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)
}
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::shader::runtime_cache::enabled()
}
fn verify_toolchain() {
static ONCE: OnceLock<()> = OnceLock::new();
ONCE.get_or_init(|| {
let current = crate::shader::compile::toolchain_id();
if crate::shader::runtime_cache::verify_toolchain(current) {
tracing::info!("shader cache: {current} did not write it, discarding entries");
}
});
}
fn load(digest: &str) -> Option<Vec<u8>> {
crate::shader::runtime_cache::load(KIND, digest).filter(|bytes| !bytes.is_empty())
}
fn store(digest: &str, bytes: &[u8]) {
crate::shader::runtime_cache::store(KIND, digest, bytes);
}
#[cfg(test)]
mod tests {
use super::*;
fn key<'a>(source: &'a str, entry: &'a str, target: &'a str) -> Key<'a> {
Key {
compiler: "hlsl",
source,
entry,
target,
}
}
#[test]
fn digest_is_stable_for_identical_inputs() {
let a = key("float4 main() { return 0; }", "main", "ps_6_0");
let b = key("float4 main() { return 0; }", "main", "ps_6_0");
assert_eq!(a.digest(), b.digest());
}
#[test]
fn every_field_changes_the_digest() {
let base = key("src", "main", "ps_6_0").digest();
assert_ne!(base, key("other", "main", "ps_6_0").digest(), "source");
assert_ne!(base, key("src", "main2", "ps_6_0").digest(), "entry");
assert_ne!(base, key("src", "main", "vs_6_0").digest(), "target");
let mut other_compiler = key("src", "main", "ps_6_0");
other_compiler.compiler = "metal";
assert_ne!(base, other_compiler.digest(), "compiler");
}
#[test]
fn field_boundaries_cannot_be_confused() {
assert_ne!(key("ab", "c", "t").digest(), key("a", "bc", "t").digest());
}
#[test]
fn the_cache_is_off_under_test() {
assert!(!enabled());
assert_eq!(load("deadbeef"), None);
}
}