use std::collections::HashMap;
use std::sync::{Arc, Mutex};
pub fn fnv1a_64(data: &[u8]) -> u64 {
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash: u64 = OFFSET_BASIS;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PipelineCacheKey {
pub shader_hash: u64,
pub entry_point: String,
pub layout_tag: String,
}
impl PipelineCacheKey {
pub fn new(shader_source: &str, entry_point: &str, layout_tag: &str) -> Self {
Self {
shader_hash: fnv1a_64(shader_source.as_bytes()),
entry_point: entry_point.to_owned(),
layout_tag: layout_tag.to_owned(),
}
}
pub fn from_hash(shader_hash: u64, entry_point: &str, layout_tag: &str) -> Self {
Self {
shader_hash,
entry_point: entry_point.to_owned(),
layout_tag: layout_tag.to_owned(),
}
}
}
impl std::fmt::Display for PipelineCacheKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:016x}:{}:{}",
self.shader_hash, self.entry_point, self.layout_tag
)
}
}
struct CacheEntry {
pipeline: Arc<wgpu::ComputePipeline>,
}
#[derive(Debug, Default)]
pub struct PipelineCache {
entries: HashMap<PipelineCacheKey, CacheEntry>,
}
impl std::fmt::Debug for CacheEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CacheEntry")
.field("pipeline", &"<wgpu::ComputePipeline>")
.finish()
}
}
impl PipelineCache {
pub fn new() -> Self {
Self::default()
}
pub fn get_or_insert_with<F, E>(
&mut self,
key: PipelineCacheKey,
factory: F,
) -> Result<Arc<wgpu::ComputePipeline>, E>
where
F: FnOnce() -> Result<wgpu::ComputePipeline, E>,
{
if let Some(entry) = self.entries.get(&key) {
tracing::trace!("Pipeline cache hit: {}", key);
return Ok(Arc::clone(&entry.pipeline));
}
tracing::debug!("Pipeline cache miss — compiling: {}", key);
let pipeline = Arc::new(factory()?);
self.entries.insert(
key,
CacheEntry {
pipeline: Arc::clone(&pipeline),
},
);
Ok(pipeline)
}
#[inline]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
tracing::debug!("Pipeline cache cleared");
}
pub fn evict(&mut self, key: &PipelineCacheKey) -> bool {
let removed = self.entries.remove(key).is_some();
if removed {
tracing::trace!("Pipeline cache evicted: {}", key);
}
removed
}
pub fn keys(&self) -> impl Iterator<Item = &PipelineCacheKey> {
self.entries.keys()
}
pub fn retain<F>(&mut self, mut predicate: F)
where
F: FnMut(&PipelineCacheKey) -> bool,
{
self.entries.retain(|k, _| predicate(k));
}
}
pub type SharedPipelineCache = Arc<Mutex<PipelineCache>>;
pub fn new_shared_pipeline_cache() -> SharedPipelineCache {
Arc::new(Mutex::new(PipelineCache::new()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fnv1a_empty_bytes_gives_offset_basis() {
let expected: u64 = 0xcbf2_9ce4_8422_2325;
assert_eq!(fnv1a_64(b""), expected);
}
#[test]
fn test_fnv1a_known_vector_hello() {
let h = fnv1a_64(b"hello");
assert_ne!(h, 0);
assert_eq!(h, fnv1a_64(b"hello"));
}
#[test]
fn test_fnv1a_different_inputs_differ() {
let h1 = fnv1a_64(b"hello");
let h2 = fnv1a_64(b"world");
assert_ne!(h1, h2, "distinct strings must produce distinct hashes");
}
#[test]
fn test_fnv1a_same_input_stable() {
let data = b"reproducible hash";
assert_eq!(fnv1a_64(data), fnv1a_64(data));
}
#[test]
fn test_fnv1a_single_byte_differ() {
let a = fnv1a_64(b"abcde");
let b = fnv1a_64(b"abcdf");
assert_ne!(a, b);
}
#[test]
fn test_fnv1a_prefix_sensitivity() {
assert_ne!(fnv1a_64(b"abc"), fnv1a_64(b"abcd"));
}
#[test]
fn test_key_equality_same_args() {
let k1 = PipelineCacheKey::new("src", "main", "r-w");
let k2 = PipelineCacheKey::new("src", "main", "r-w");
assert_eq!(k1, k2);
}
#[test]
fn test_key_inequality_different_source() {
let k1 = PipelineCacheKey::new("shader_a", "main", "r-w");
let k2 = PipelineCacheKey::new("shader_b", "main", "r-w");
assert_ne!(k1, k2);
}
#[test]
fn test_key_inequality_different_entry() {
let k1 = PipelineCacheKey::new("src", "entry_a", "r-w");
let k2 = PipelineCacheKey::new("src", "entry_b", "r-w");
assert_ne!(k1, k2);
}
#[test]
fn test_key_inequality_different_layout_tag() {
let k1 = PipelineCacheKey::new("src", "main", "r-w");
let k2 = PipelineCacheKey::new("src", "main", "r-r-w");
assert_ne!(k1, k2);
}
#[test]
fn test_key_shader_hash_matches_fnv1a() {
let src = "// some wgsl source";
let key = PipelineCacheKey::new(src, "compute", "r-w");
assert_eq!(key.shader_hash, fnv1a_64(src.as_bytes()));
}
#[test]
fn test_key_from_hash_constructor() {
let hash: u64 = 0xdeadbeef_cafebabe;
let key = PipelineCacheKey::from_hash(hash, "ep", "u-r-w");
assert_eq!(key.shader_hash, hash);
assert_eq!(key.entry_point, "ep");
assert_eq!(key.layout_tag, "u-r-w");
}
#[test]
fn test_key_display_format() {
let key = PipelineCacheKey::from_hash(0x0123_4567_89ab_cdef, "cs_main", "r-w");
let s = format!("{}", key);
assert!(s.contains("0123456789abcdef"));
assert!(s.contains("cs_main"));
assert!(s.contains("r-w"));
}
#[test]
fn test_key_clone_equality() {
let k = PipelineCacheKey::new("src", "main", "r-w");
assert_eq!(k.clone(), k);
}
#[test]
fn test_new_cache_is_empty() {
let cache = PipelineCache::new();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_default_cache_is_empty() {
let cache: PipelineCache = Default::default();
assert!(cache.is_empty());
}
#[test]
fn test_evict_absent_key_returns_false() {
let mut cache = PipelineCache::new();
let key = PipelineCacheKey::new("src", "main", "r-w");
assert!(!cache.evict(&key));
assert_eq!(cache.len(), 0);
}
#[test]
fn test_clear_on_empty_cache_is_noop() {
let mut cache = PipelineCache::new();
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_retain_on_empty_cache_is_noop() {
let mut cache = PipelineCache::new();
cache.retain(|_| false);
assert!(cache.is_empty());
}
#[test]
fn test_cache_miss_calls_factory_once() {
let mut cache = PipelineCache::new();
let key = PipelineCacheKey::new("src", "main", "r-w");
let call_count = std::cell::Cell::new(0u32);
let result: Result<Arc<wgpu::ComputePipeline>, &str> =
cache.get_or_insert_with(key.clone(), || {
call_count.set(call_count.get() + 1);
Err("no gpu in test")
});
assert!(result.is_err());
assert_eq!(call_count.get(), 1, "factory must be called exactly once");
assert!(cache.is_empty(), "failed factory must not pollute cache");
}
#[test]
fn test_cache_error_does_not_store_entry() {
let mut cache = PipelineCache::new();
let key = PipelineCacheKey::new("shader", "ep", "r-r-w");
let _: Result<Arc<wgpu::ComputePipeline>, &str> =
cache.get_or_insert_with(key.clone(), || Err("compile error"));
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_new_shared_pipeline_cache_is_empty() {
let shared = new_shared_pipeline_cache();
#[allow(clippy::unwrap_used)]
let guard = shared.lock().map_err(|_| "poisoned").unwrap();
assert!(guard.is_empty());
}
#[test]
fn test_shared_cache_is_arc_mutex() {
let cache = new_shared_pipeline_cache();
let cache2 = Arc::clone(&cache);
assert!(Arc::ptr_eq(&cache, &cache2));
}
}