Skip to main content

alpha2_render/
result_cache.rs

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