use std::collections::HashMap;
use crate::RenderedTexture;
use crate::graph::RenderCacheKey;
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum CacheIntent {
#[default]
Auto,
Pin,
Bypass,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum CacheEligibility {
Eligible,
TransientSource,
Bypassed,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum CacheInvalidation {
Effects,
EffectResources,
Quality,
Explicit,
GpuError,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct ResultCacheStats {
pub budget_bytes: u64,
pub bytes: u64,
pub entries: u64,
pub pinned: u64,
pub hits: u64,
pub misses: u64,
pub ineligible: u64,
pub inserts: u64,
pub evictions: u64,
pub skipped_nodes: u64,
pub invalidations: [u64; 5],
}
struct Entry {
texture: RenderedTexture,
bytes: u64,
pinned: bool,
last_used: u64,
}
#[derive(Default)]
pub(crate) struct ResultCache {
entries: HashMap<u128, Entry>,
clock: u64,
stats: ResultCacheStats,
}
impl std::fmt::Debug for ResultCache {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ResultCache")
.field("stats", &self.stats)
.finish_non_exhaustive()
}
}
impl ResultCache {
pub(crate) const fn enabled(&self) -> bool {
self.stats.budget_bytes > 0
}
pub(crate) const fn stats(&self) -> ResultCacheStats {
self.stats
}
pub(crate) fn set_budget(&mut self, budget_bytes: u64) {
self.stats.budget_bytes = budget_bytes;
self.evict_to(budget_bytes);
}
pub(crate) fn get(&mut self, key: RenderCacheKey, pinned: bool) -> Option<RenderedTexture> {
self.clock += 1;
let clock = self.clock;
let entry = self.entries.get_mut(&key.value())?;
entry.last_used = clock;
if entry.pinned != pinned {
entry.pinned = pinned;
if pinned {
self.stats.pinned += 1;
} else {
self.stats.pinned -= 1;
}
}
Some(entry.texture.clone())
}
pub(crate) fn insert(&mut self, key: RenderCacheKey, texture: &RenderedTexture, pinned: bool) {
let bytes = texture_bytes(texture);
if bytes > self.stats.budget_bytes {
return;
}
self.remove(key.value());
self.evict_to(self.stats.budget_bytes - bytes);
self.clock += 1;
self.entries.insert(
key.value(),
Entry {
texture: texture.clone(),
bytes,
pinned,
last_used: self.clock,
},
);
self.stats.bytes += bytes;
self.stats.entries += 1;
self.stats.pinned += u64::from(pinned);
self.stats.inserts += 1;
}
pub(crate) fn clear(&mut self, reason: CacheInvalidation) {
if !self.entries.is_empty() {
self.stats.invalidations[reason as usize] += 1;
}
self.entries.clear();
self.stats.bytes = 0;
self.stats.entries = 0;
self.stats.pinned = 0;
}
pub(crate) fn record(&mut self, hits: u64, misses: u64, ineligible: u64, skipped: u64) {
self.stats.hits += hits;
self.stats.misses += misses;
self.stats.ineligible += ineligible;
self.stats.skipped_nodes += skipped;
}
fn evict_to(&mut self, bytes: u64) {
while self.stats.bytes > bytes {
let Some(victim) = self
.entries
.iter()
.min_by_key(|(_, entry)| (entry.pinned, entry.last_used))
.map(|(key, _)| *key)
else {
return;
};
self.remove(victim);
self.stats.evictions += 1;
}
}
fn remove(&mut self, key: u128) {
if let Some(entry) = self.entries.remove(&key) {
self.stats.bytes -= entry.bytes;
self.stats.entries -= 1;
self.stats.pinned -= u64::from(entry.pinned);
}
}
}
fn texture_bytes(texture: &RenderedTexture) -> u64 {
let texel = u64::from(texture.format.block_copy_size(None).unwrap_or(4));
u64::from(texture.size.width) * u64::from(texture.size.height) * texel
}