alpha2_render/
result_cache.rs1use std::collections::HashMap;
13
14use crate::RenderedTexture;
15use crate::graph::RenderCacheKey;
16
17#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
19pub enum CacheIntent {
20 #[default]
22 Auto,
23 Pin,
25 Bypass,
27}
28
29#[derive(Copy, Clone, Debug, Eq, PartialEq)]
31pub enum CacheEligibility {
32 Eligible,
34 TransientSource,
36 Bypassed,
38}
39
40#[derive(Copy, Clone, Debug, Eq, PartialEq)]
42pub enum CacheInvalidation {
43 Effects,
45 EffectResources,
47 Quality,
49 Explicit,
51 GpuError,
54}
55
56#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
58pub struct ResultCacheStats {
59 pub budget_bytes: u64,
61 pub bytes: u64,
63 pub entries: u64,
65 pub pinned: u64,
67 pub hits: u64,
69 pub misses: u64,
71 pub ineligible: u64,
73 pub inserts: u64,
75 pub evictions: u64,
77 pub skipped_nodes: u64,
79 pub invalidations: [u64; 5],
81}
82
83struct Entry<T> {
84 texture: T,
85 bytes: u64,
86 pinned: bool,
87 last_used: u64,
88}
89
90pub(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: 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 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 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}