alpha2_render/
result_cache.rs1use std::collections::HashMap;
18
19use crate::RenderedTexture;
20use crate::graph::RenderCacheKey;
21
22#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
24pub enum CacheIntent {
25 #[default]
27 Auto,
28 Pin,
31 Bypass,
34}
35
36#[derive(Copy, Clone, Debug, Eq, PartialEq)]
38pub enum CacheEligibility {
39 Eligible,
41 TransientSource,
44 Bypassed,
47}
48
49#[derive(Copy, Clone, Debug, Eq, PartialEq)]
51pub enum CacheInvalidation {
52 Effects,
54 EffectResources,
56 Quality,
58 Explicit,
60 GpuError,
63}
64
65#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
67pub struct ResultCacheStats {
68 pub budget_bytes: u64,
70 pub bytes: u64,
72 pub entries: u64,
74 pub pinned: u64,
76 pub hits: u64,
78 pub misses: u64,
80 pub ineligible: u64,
83 pub inserts: u64,
85 pub evictions: u64,
87 pub skipped_nodes: u64,
89 pub invalidations: [u64; 5],
92}
93
94struct Entry<T> {
95 texture: T,
96 bytes: u64,
97 pinned: bool,
98 last_used: u64,
99}
100
101pub(crate) trait CachedPicture: Clone {
105 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: 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 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 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}