Skip to main content

alpha2_render/
result_cache.rs

1//! Rendered node results kept across graph executions.
2//!
3//! A node's key covers its operation, parameters, runtime, size and its inputs' keys down to
4//! the sources. It is complete only when every source beneath has a host-vouched key
5//! ([`crate::RenderGraph::add_source_with_key`]); unkeyed sources get a per-graph key. Only
6//! candidate nodes ([`crate::RenderGraph::set_cache_intent`]) with complete keys are stored, so
7//! a hit is exactly what a live render would produce.
8//!
9//! Anything the key leaves out clears the cache when it changes: effect removal, effect
10//! resource changes, and quality controls.
11
12use std::collections::HashMap;
13
14use crate::RenderedTexture;
15use crate::graph::RenderCacheKey;
16
17/// Whether a node's result may be kept and reused.
18#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
19pub enum CacheIntent {
20    /// Keep when the key is complete; evict like any other result.
21    #[default]
22    Auto,
23    /// Keep when the key is complete; evict only after every unpinned result.
24    Pin,
25    /// Never read or keep it, its inputs, or its dependents; for inspecting a live render.
26    Bypass,
27}
28
29/// Whether a node's result can be cached, and if not, why.
30#[derive(Copy, Clone, Debug, Eq, PartialEq)]
31pub enum CacheEligibility {
32    /// Its key names everything it depends on.
33    Eligible,
34    /// A source beneath it has no stable key.
35    TransientSource,
36    /// It, an input, or a dependent asks for [`CacheIntent::Bypass`].
37    Bypassed,
38}
39
40/// Why cached results were dropped all at once.
41#[derive(Copy, Clone, Debug, Eq, PartialEq)]
42pub enum CacheInvalidation {
43    /// An effect was registered or removed.
44    Effects,
45    /// An effect resource was uploaded, replaced, or removed.
46    EffectResources,
47    /// Filter or temporal sampling controls changed.
48    Quality,
49    /// The caller cleared it.
50    Explicit,
51    /// The browser reported a GPU error for earlier work (see
52    /// [`crate::Renderer::gpu_error_reports`]).
53    GpuError,
54}
55
56/// Result-cache counters since the renderer was created.
57#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
58pub struct ResultCacheStats {
59    /// Bytes results may occupy; zero disables the cache.
60    pub budget_bytes: u64,
61    /// Bytes the kept results occupy.
62    pub bytes: u64,
63    /// Results kept.
64    pub entries: u64,
65    /// Of those, pinned.
66    pub pinned: u64,
67    /// Candidate nodes found in the cache.
68    pub hits: u64,
69    /// Candidate nodes with complete keys that were not.
70    pub misses: u64,
71    /// Candidates that could not be cached (transient source or bypass).
72    pub ineligible: u64,
73    /// Results stored.
74    pub inserts: u64,
75    /// Results dropped to stay within the budget.
76    pub evictions: u64,
77    /// Nodes not encoded because a result above them was found.
78    pub skipped_nodes: u64,
79    /// Clears by reason: effects, effect resources, quality, explicit, GPU error.
80    pub invalidations: [u64; 5],
81}
82
83struct Entry<T> {
84    texture: T,
85    bytes: u64,
86    pinned: bool,
87    last_used: u64,
88}
89
90/// A picture the result cache can keep, cloned out on a hit and charged against the budget.
91pub(crate) trait CachedPicture: Clone {
92    fn cached_bytes(&self) -> u64;
93}
94
95impl CachedPicture for RenderedTexture {
96    fn cached_bytes(&self) -> u64 {
97        texture_bytes(self)
98    }
99}
100
101pub(crate) struct ResultCache<T = RenderedTexture> {
102    entries: HashMap<u128, Entry<T>>,
103    clock: u64,
104    /// Clears for any reason, even when empty; each changes pixels without changing graph keys.
105    clears: u64,
106    stats: ResultCacheStats,
107}
108
109impl<T> Default for ResultCache<T> {
110    fn default() -> Self {
111        Self {
112            entries: HashMap::new(),
113            clock: 0,
114            clears: 0,
115            stats: ResultCacheStats::default(),
116        }
117    }
118}
119
120impl<T> std::fmt::Debug for ResultCache<T> {
121    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        formatter
123            .debug_struct("ResultCache")
124            .field("stats", &self.stats)
125            .finish_non_exhaustive()
126    }
127}
128
129impl<T: CachedPicture> ResultCache<T> {
130    pub(crate) const fn enabled(&self) -> bool {
131        self.stats.budget_bytes > 0
132    }
133
134    pub(crate) const fn stats(&self) -> ResultCacheStats {
135        self.stats
136    }
137
138    pub(crate) const fn clears(&self) -> u64 {
139        self.clears
140    }
141
142    pub(crate) fn set_budget(&mut self, budget_bytes: u64) {
143        self.stats.budget_bytes = budget_bytes;
144        self.evict_to(budget_bytes);
145    }
146
147    /// A kept result, marked used and re-pinned per the current request (intent is not part of the
148    /// key).
149    pub(crate) fn get(&mut self, key: RenderCacheKey, pinned: bool) -> Option<T> {
150        self.clock += 1;
151        let clock = self.clock;
152        let entry = self.entries.get_mut(&key.value())?;
153        entry.last_used = clock;
154        if entry.pinned != pinned {
155            entry.pinned = pinned;
156            if pinned {
157                self.stats.pinned += 1;
158            } else {
159                self.stats.pinned -= 1;
160            }
161        }
162        Some(entry.texture.clone())
163    }
164
165    /// Keeps `texture` under `key`, evicting least recently used results (unpinned first). A
166    /// result larger than the whole budget is not kept.
167    pub(crate) fn insert(&mut self, key: RenderCacheKey, texture: &T, pinned: bool) {
168        let bytes = texture.cached_bytes();
169        if bytes > self.stats.budget_bytes {
170            return;
171        }
172        self.remove(key.value());
173        self.evict_to(self.stats.budget_bytes - bytes);
174        self.clock += 1;
175        self.entries.insert(
176            key.value(),
177            Entry {
178                texture: texture.clone(),
179                bytes,
180                pinned,
181                last_used: self.clock,
182            },
183        );
184        self.stats.bytes += bytes;
185        self.stats.entries += 1;
186        self.stats.pinned += u64::from(pinned);
187        self.stats.inserts += 1;
188    }
189
190    pub(crate) fn clear(&mut self, reason: CacheInvalidation) {
191        self.clears += 1;
192        if !self.entries.is_empty() {
193            self.stats.invalidations[reason as usize] += 1;
194        }
195        self.entries.clear();
196        self.stats.bytes = 0;
197        self.stats.entries = 0;
198        self.stats.pinned = 0;
199    }
200
201    pub(crate) fn record(&mut self, hits: u64, misses: u64, ineligible: u64, skipped: u64) {
202        self.stats.hits += hits;
203        self.stats.misses += misses;
204        self.stats.ineligible += ineligible;
205        self.stats.skipped_nodes += skipped;
206    }
207
208    fn evict_to(&mut self, bytes: u64) {
209        while self.stats.bytes > bytes {
210            let Some(victim) = self
211                .entries
212                .iter()
213                .min_by_key(|(_, entry)| (entry.pinned, entry.last_used))
214                .map(|(key, _)| *key)
215            else {
216                return;
217            };
218            self.remove(victim);
219            self.stats.evictions += 1;
220        }
221    }
222
223    fn remove(&mut self, key: u128) {
224        if let Some(entry) = self.entries.remove(&key) {
225            self.stats.bytes -= entry.bytes;
226            self.stats.entries -= 1;
227            self.stats.pinned -= u64::from(entry.pinned);
228        }
229    }
230}
231
232fn texture_bytes(texture: &RenderedTexture) -> u64 {
233    let texel = u64::from(texture.format.block_copy_size(None).unwrap_or(4));
234    u64::from(texture.size.width) * u64::from(texture.size.height) * texel
235}