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 {
95 texture: RenderedTexture,
96 bytes: u64,
97 pinned: bool,
98 last_used: u64,
99}
100
101/// Kept results by renderer-level key, bounded by bytes.
102#[derive(Default)]
103pub(crate) struct ResultCache {
104 entries: HashMap<u128, Entry>,
105 clock: u64,
106 /// Clears for any reason, whether or not anything was kept: each is an
107 /// event that changes pixels without changing graph keys.
108 clears: u64,
109 stats: ResultCacheStats,
110}
111
112impl std::fmt::Debug for ResultCache {
113 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 formatter
115 .debug_struct("ResultCache")
116 .field("stats", &self.stats)
117 .finish_non_exhaustive()
118 }
119}
120
121impl ResultCache {
122 pub(crate) const fn enabled(&self) -> bool {
123 self.stats.budget_bytes > 0
124 }
125
126 pub(crate) const fn stats(&self) -> ResultCacheStats {
127 self.stats
128 }
129
130 pub(crate) const fn clears(&self) -> u64 {
131 self.clears
132 }
133
134 pub(crate) fn set_budget(&mut self, budget_bytes: u64) {
135 self.stats.budget_bytes = budget_bytes;
136 self.evict_to(budget_bytes);
137 }
138
139 /// A kept result, marked as just used and pinned as the node that asks
140 /// for it now is: a cache intent is not part of the key, so a clip pinned
141 /// or unpinned after its picture was kept finds the same entry.
142 pub(crate) fn get(&mut self, key: RenderCacheKey, pinned: bool) -> Option<RenderedTexture> {
143 self.clock += 1;
144 let clock = self.clock;
145 let entry = self.entries.get_mut(&key.value())?;
146 entry.last_used = clock;
147 if entry.pinned != pinned {
148 entry.pinned = pinned;
149 if pinned {
150 self.stats.pinned += 1;
151 } else {
152 self.stats.pinned -= 1;
153 }
154 }
155 Some(entry.texture.clone())
156 }
157
158 /// Keeps `texture` under `key`, evicting least recently used results
159 /// (unpinned before pinned) to make room. A result larger than the whole
160 /// budget is not kept.
161 pub(crate) fn insert(&mut self, key: RenderCacheKey, texture: &RenderedTexture, pinned: bool) {
162 let bytes = texture_bytes(texture);
163 if bytes > self.stats.budget_bytes {
164 return;
165 }
166 self.remove(key.value());
167 self.evict_to(self.stats.budget_bytes - bytes);
168 self.clock += 1;
169 self.entries.insert(
170 key.value(),
171 Entry {
172 texture: texture.clone(),
173 bytes,
174 pinned,
175 last_used: self.clock,
176 },
177 );
178 self.stats.bytes += bytes;
179 self.stats.entries += 1;
180 self.stats.pinned += u64::from(pinned);
181 self.stats.inserts += 1;
182 }
183
184 pub(crate) fn clear(&mut self, reason: CacheInvalidation) {
185 self.clears += 1;
186 if !self.entries.is_empty() {
187 self.stats.invalidations[reason as usize] += 1;
188 }
189 self.entries.clear();
190 self.stats.bytes = 0;
191 self.stats.entries = 0;
192 self.stats.pinned = 0;
193 }
194
195 pub(crate) fn record(&mut self, hits: u64, misses: u64, ineligible: u64, skipped: u64) {
196 self.stats.hits += hits;
197 self.stats.misses += misses;
198 self.stats.ineligible += ineligible;
199 self.stats.skipped_nodes += skipped;
200 }
201
202 fn evict_to(&mut self, bytes: u64) {
203 while self.stats.bytes > bytes {
204 let Some(victim) = self
205 .entries
206 .iter()
207 .min_by_key(|(_, entry)| (entry.pinned, entry.last_used))
208 .map(|(key, _)| *key)
209 else {
210 return;
211 };
212 self.remove(victim);
213 self.stats.evictions += 1;
214 }
215 }
216
217 fn remove(&mut self, key: u128) {
218 if let Some(entry) = self.entries.remove(&key) {
219 self.stats.bytes -= entry.bytes;
220 self.stats.entries -= 1;
221 self.stats.pinned -= u64::from(entry.pinned);
222 }
223 }
224}
225
226fn texture_bytes(texture: &RenderedTexture) -> u64 {
227 let texel = u64::from(texture.format.block_copy_size(None).unwrap_or(4));
228 u64::from(texture.size.width) * u64::from(texture.size.height) * texel
229}