frust_engine/cache/retained.rs
1//! Retained pages: bodies the renderer rasterized once and keeps across
2//! frames, so a later frame can composite them instead of painting them again.
3//!
4//! A `PushRetained` bracket's body is an ordinary layer of the frame — the
5//! compiler records it as one whatever its alpha (see
6//! [`crate::compile::layers`]) — and this store decides, per frame, whether
7//! that layer renders into a pooled page like any other (the inline lowering)
8//! or into a page the store owns and keeps. A `DrawRetained` names a page by
9//! key; the store resolves it to a composite of that page, or refuses the
10//! frame when it cannot.
11//!
12//! ## What a page holds, and what it can be reused for
13//!
14//! A page holds the body exactly as the inline lowering rasterizes it: on the
15//! device grid, under the bracket's own transform with its presentation scale
16//! already applied, at the layer's own tile-aligned bounds, and **without** the
17//! bracket's alpha — the alpha is the composite's, so a page fades at no cost.
18//! It is drawn through the same 1:1 [`PaintClass::LayerComposite`] quad a
19//! pooled page is, never resampled, which is what makes a retained frame
20//! pixel-identical to the inline one and also bounds what a page can serve: a
21//! `DrawRetained` whose [`RetainedPlacement`] has the page's linear part
22//! (scale, rotation, skew) and whose translation differs by any amount is
23//! composited at the page's bounds moved by that translation rounded to whole
24//! pixels; anything else — another scale, another extent — is not resident at
25//! that placement, which is a refusal (below). The resident-key ledger reports
26//! each page's extent and scale *as rasterized*, presentation scale included,
27//! so a widget asking for the extent it is about to draw at only ever asks for
28//! a page that can serve it.
29//!
30//! [`PaintClass::LayerComposite`]: crate::gpu::strips::PaintClass::LayerComposite
31//!
32//! ## Outside the pool, outside the page budget
33//!
34//! A page is this store's own allocation (`gpu::targets::retained_page_descriptor`),
35//! never a pooled entry: the pool ages and re-lets textures frame by frame,
36//! and a retained page's whole point is to survive across frames. For the same
37//! reason it is **not** one of the scheduler's live pages: a round rendering a
38//! body into its retained page, and the composite that later samples it, take
39//! nothing from the two ping-pong groups or the spill page
40//! ([`MAX_LIVE_PAGES`](crate::schedule::MAX_LIVE_PAGES)) — the body's own nested
41//! translucent layers still do, exactly as they would inside any layer.
42//!
43//! ## Budget, eviction and refusals
44//!
45//! The store is held to a byte budget ([`RetainedBudget`]) tiered by adapter
46//! family the way the image atlas is (`cache::images::choose_tier`): a phone
47//! keeps a few full-screen pages, a desktop a few more. A body that needs a
48//! page evicts the least recently used pages until it fits, never one the frame
49//! being planned draws or records. A body is refused — rendered inline this
50//! frame, its key absent from the ledger — when the compiler marked it
51//! unretainable (clipped on the way in, a hole punch or an external texture
52//! inside it, an unbalanced bracket), when it drew nothing, drew outside its
53//! own rectangle, is larger than [`MAX_INTERMEDIATE_DIMENSION`] or the
54//! adapter's ceiling on either axis, needs more than the whole budget, or names
55//! a key the same frame already records or draws.
56//!
57//! ## A page holds this frame's recording, or goes
58//!
59//! The ledger is a promise to the widget tree: a key it lists can be drawn
60//! instead of painted. So after any frame carrying a `PushRetained` for a key,
61//! the ledger lists that key only if its page holds *that frame's* recording
62//! of the body — a page holding an older recording would composite stale
63//! pixels on the next frame the widget asks for it. A re-record whose
64//! recording does not reach the page therefore evicts the key's page rather
65//! than keep it:
66//!
67//! - **refused** — any [`RetainedRefusal`], the body rendered inline;
68//! - **nested** — a bracket inside another one, which the compiler folds into
69//! the outermost body and lists no bracket for
70//! ([`CompiledFrame::retained_superseded`](crate::compile::CompiledFrame::retained_superseded));
71//! - **unrendered** — an admitted body inside a layer the schedule dropped.
72//!
73//! A key some bracket of the same frame draws, or records into its page and
74//! renders, keeps its page: that page is what the frame composited or
75//! recorded under the key. A key the frame does not name at all is untouched,
76//! and ages out least recently used like any other.
77//!
78//! A `DrawRetained` the store cannot serve is a renderer error the display
79//! list's own contract anticipates: the frame is refused with
80//! [`EngineError::RetainedKeyMissing`] before anything is recorded, and the
81//! store is cleared, so the ledger published after it is empty and the next
82//! frame paints every body again.
83//!
84//! ## Plan, then commit
85//!
86//! [`RetainedPages::plan`] decides everything from plain values and changes
87//! nothing of the store but its frame clock (and, on a refusal, its contents) —
88//! of the frame, only the bounds of the recorded layers enclosing a served
89//! `DrawRetained`, grown to hold its composite, since the empty layer standing
90//! for it put nothing of it into theirs:
91//! a frame can still be refused after it — by the scheduler, or a capacity
92//! check — and a refused frame must leave every page and every ledger entry as
93//! it was. [`RetainedPages::commit`] applies a plan once nothing can fail any
94//! more: evictions first — the plan's least recently used ones and every
95//! refused, nested or unrendered re-record's key (above) — then each admitted
96//! body's page — reused when the key's current page is large enough, allocated
97//! otherwise — and only for a body the schedule really rendered (a body inside
98//! a dropped layer renders nothing, and a page claiming its key would
99//! composite stale texels).
100//!
101//! Generic over the texture and view types like the intermediate pool, so plan,
102//! eviction and ledger behaviour is exercised against a counting fake
103//! allocator with no GPU in the loop (`tests/retained.rs`).
104
105use frust_gpu::{TextureAllocator, TierCaps};
106use vello_common::geometry::RectU16;
107use vello_common::record::RecordedLayer;
108use vello_common::tile::Tile;
109
110use crate::cache::images::{AtlasTier, choose_tier, device_memory_bytes};
111use crate::compile::{RetainedBracket, RetainedKind, RetainedPlacement};
112use crate::config;
113use crate::error::EngineError;
114use crate::gpu::targets::{MAX_INTERMEDIATE_DIMENSION, max_texture_size, retained_page_descriptor};
115
116/// Bytes one retained-page texel costs — the intermediate format is
117/// `Rgba8Unorm`.
118pub const RETAINED_BYTES_PER_TEXEL: u64 = 4;
119
120/// How many bytes of retained pages a store may hold at once.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct RetainedBudget {
123 /// The ceiling, in bytes of texture.
124 pub bytes: u64,
125}
126
127impl RetainedBudget {
128 /// A phone's budget: 32 MiB, three 1080x2400 full-screen pages — what a
129 /// navigator transition (the outgoing and the incoming page) needs with
130 /// one to spare.
131 pub const MOBILE: Self = Self { bytes: 32 << 20 };
132
133 /// A phone with at least 4 GiB of memory: 64 MiB.
134 pub const MOBILE_LARGE: Self = Self { bytes: 64 << 20 };
135
136 /// A desktop-class adapter: 256 MiB, several 4K-window pages.
137 pub const DESKTOP: Self = Self { bytes: 256 << 20 };
138
139 /// The budget `tier` stands for — the image atlas's own tier read
140 /// (`cache::images::choose_tier`), so one adapter answers the
141 /// mobile-or-desktop question once for both caches.
142 #[must_use]
143 pub fn for_tier(tier: AtlasTier) -> Self {
144 match tier {
145 AtlasTier::Mobile => Self::MOBILE,
146 AtlasTier::MobileLarge => Self::MOBILE_LARGE,
147 AtlasTier::Desktop => Self::DESKTOP,
148 }
149 }
150
151 /// The budget for `caps`' adapter on this device, or
152 /// `FRUST_ENGINE_RETAINED_BUDGET_MB` when it is set.
153 #[must_use]
154 pub fn for_caps(caps: &TierCaps) -> Self {
155 match config::retained_budget_mb() {
156 Some(mib) => Self {
157 bytes: mib.saturating_mul(1 << 20),
158 },
159 None => Self::for_tier(choose_tier(caps, device_memory_bytes()).tier),
160 }
161 }
162}
163
164/// Why a body was rendered inline rather than into a retained page.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum RetainedRefusal {
167 /// The compiler marked the body unretainable (see
168 /// [`RetainedKind::Record`]).
169 NotRetainable,
170 /// The body drew nothing at all.
171 Empty,
172 /// The body drew outside its own rectangle.
173 Overflow,
174 /// The body's bounds exceed the largest page on either axis.
175 TooLarge,
176 /// The body's page needs more than the whole budget, or more than every
177 /// evictable page frees.
178 OverBudget,
179 /// The frame already records or draws the same key.
180 Duplicate,
181}
182
183/// What the store decided for one retained bracket of a frame.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum RetainedDecision {
186 /// A body rendered inline — the ordinary layer its bracket lowers to.
187 Inline(RetainedRefusal),
188 /// A body rendered into the key's retained page, which holds `bounds`
189 /// at its own origin.
190 Record {
191 /// The body's tile-aligned device bounds.
192 bounds: RectU16,
193 },
194 /// A `DrawRetained` composited from the key's resident page.
195 Draw {
196 /// Where the composite lands, already trimmed to the command's window.
197 dest: RectU16,
198 /// The page texel under `dest`'s top-left corner.
199 origin: (u16, u16),
200 },
201 /// A `DrawRetained` whose composite would land wholly outside its window:
202 /// the page is resident and served, and draws nothing this frame.
203 Hidden,
204}
205
206/// One frame's decisions, parallel to the frame's
207/// [`CompiledFrame::retained`](crate::compile::CompiledFrame::retained), and
208/// the keys committing it evicts.
209#[derive(Debug, Clone, Default, PartialEq, Eq)]
210pub struct RetainedPlan {
211 /// One decision per retained bracket, in recording order.
212 pub decisions: Vec<RetainedDecision>,
213 /// The keys whose pages committing this plan drops, least recently used
214 /// first.
215 pub evictions: Vec<u64>,
216}
217
218/// One resident page as the resident-key ledger reports it.
219#[derive(Debug, Clone, Copy, PartialEq)]
220pub struct RetainedEntry {
221 /// The raw key the page was recorded under.
222 pub key: u64,
223 /// The extent the bracket's rectangle covered when the page was rendered,
224 /// in device pixels.
225 pub extent_px: (u32, u32),
226 /// The scale the page was rasterized at — device scale and presentation
227 /// scale together (see [`RetainedPlacement::scale`]).
228 pub scale: f32,
229}
230
231/// One page the store holds.
232#[derive(Debug)]
233struct RetainedPage<T, V> {
234 key: u64,
235 /// The body's tile-aligned device bounds when it was rendered; the page
236 /// holds them at its own origin.
237 bounds: RectU16,
238 /// Where the body's local space landed when it was rendered.
239 placement: RetainedPlacement,
240 /// The ledger's extent.
241 extent_px: (u32, u32),
242 /// The texture's own extent, at least `bounds`' on both axes.
243 size: (u32, u32),
244 /// The store frame the page was last recorded or drawn on.
245 last_used: u64,
246 /// Kept alive beside its view; never read.
247 _texture: T,
248 view: V,
249}
250
251impl<T, V> RetainedPage<T, V> {
252 fn bytes(&self) -> u64 {
253 page_bytes(self.size)
254 }
255
256 fn entry(&self) -> RetainedEntry {
257 RetainedEntry {
258 key: self.key,
259 extent_px: self.extent_px,
260 scale: self.placement.scale(),
261 }
262 }
263}
264
265/// The renderer's retained pages: a budgeted, least-recently-used store keyed
266/// by the raw retained key (see the module doc).
267#[derive(Debug)]
268pub struct RetainedPages<T = wgpu::Texture, V = wgpu::TextureView> {
269 pages: Vec<RetainedPage<T, V>>,
270 budget: RetainedBudget,
271 /// The largest page extent per axis — the smaller of the adapter's ceiling
272 /// and [`MAX_INTERMEDIATE_DIMENSION`].
273 max_dimension: u32,
274 /// Advanced once per [`Self::plan`]; what `last_used` is stamped with.
275 frame: u64,
276 /// Advanced whenever what the ledger reports changes.
277 generation: u64,
278 /// Per retained bracket of the last committed frame, the index of the page
279 /// it renders into or composites from.
280 bound: Vec<Option<usize>>,
281 /// Whether a `DrawRetained` refusal cleared the store since the last
282 /// [`Self::take_refusal`].
283 refused: bool,
284}
285
286impl<T, V> RetainedPages<T, V> {
287 /// An empty store held to `budget`, with pages capped at `max_dimension`
288 /// per axis (and never above [`MAX_INTERMEDIATE_DIMENSION`]).
289 #[must_use]
290 pub fn new(budget: RetainedBudget, max_dimension: u32) -> Self {
291 Self {
292 pages: Vec::new(),
293 budget,
294 max_dimension: max_dimension.min(MAX_INTERMEDIATE_DIMENSION),
295 frame: 0,
296 generation: 0,
297 bound: Vec::new(),
298 refused: false,
299 }
300 }
301
302 /// An empty store budgeted and capped for `caps`' adapter.
303 #[must_use]
304 pub fn for_caps(caps: &TierCaps) -> Self {
305 Self::new(RetainedBudget::for_caps(caps), max_texture_size(caps))
306 }
307
308 /// The budget the store is held to.
309 #[must_use]
310 pub fn budget(&self) -> RetainedBudget {
311 self.budget
312 }
313
314 /// Re-budget the store, evicting least recently used pages until what it
315 /// holds fits.
316 pub fn set_budget(&mut self, budget: RetainedBudget) {
317 self.budget = budget;
318 let mut order: Vec<(u64, u64)> = self
319 .pages
320 .iter()
321 .map(|page| (page.last_used, page.key))
322 .collect();
323 order.sort_unstable();
324 for (_, key) in order {
325 if self.used_bytes() <= budget.bytes {
326 break;
327 }
328 self.remove(key);
329 }
330 }
331
332 /// The largest page extent the store allocates, per axis.
333 #[must_use]
334 pub fn max_dimension(&self) -> u32 {
335 self.max_dimension
336 }
337
338 /// How many pages are resident.
339 #[must_use]
340 pub fn len(&self) -> usize {
341 self.pages.len()
342 }
343
344 /// Whether no page is resident.
345 #[must_use]
346 pub fn is_empty(&self) -> bool {
347 self.pages.is_empty()
348 }
349
350 /// The bytes of texture the resident pages hold.
351 #[must_use]
352 pub fn used_bytes(&self) -> u64 {
353 self.pages.iter().map(RetainedPage::bytes).sum()
354 }
355
356 /// Whether `key` has a resident page.
357 #[must_use]
358 pub fn contains(&self, key: u64) -> bool {
359 self.position(key).is_some()
360 }
361
362 /// The store's frame clock: how many frames have been planned.
363 #[must_use]
364 pub fn frame(&self) -> u64 {
365 self.frame
366 }
367
368 /// A counter advanced whenever the set [`Self::entries`] reports changes —
369 /// a page added, evicted, re-rendered at another extent or scale, or the
370 /// store cleared. A caller publishing the ledger only when this moved
371 /// publishes every change and nothing else.
372 #[must_use]
373 pub fn generation(&self) -> u64 {
374 self.generation
375 }
376
377 /// The resident pages, as the ledger reports them, in no particular order.
378 pub fn entries(&self) -> impl Iterator<Item = RetainedEntry> + '_ {
379 self.pages.iter().map(RetainedPage::entry)
380 }
381
382 /// Whether a `DrawRetained` refusal cleared the store since the last call,
383 /// clearing the flag — what lets a host ask for one more frame to be
384 /// painted against the emptied ledger.
385 pub fn take_refusal(&mut self) -> bool {
386 std::mem::take(&mut self.refused)
387 }
388
389 /// Drop every page.
390 ///
391 /// What a surface recreate, a resize and a `DrawRetained` refusal all do:
392 /// the ledger the widget tree paints against goes empty with it, and every
393 /// body is painted (and, where admitted, recorded) again.
394 pub fn clear(&mut self) {
395 if !self.pages.is_empty() {
396 self.generation = self.generation.wrapping_add(1);
397 }
398 self.pages.clear();
399 self.bound.clear();
400 }
401
402 /// The view and texture extent of the page retained bracket `bracket` of
403 /// the last committed frame renders into or composites from.
404 #[must_use]
405 pub fn page_for(&self, bracket: u32) -> Option<(&V, (u32, u32))> {
406 let index = (*self.bound.get(bracket as usize)?)?;
407 let page = self.pages.get(index)?;
408 Some((&page.view, page.size))
409 }
410
411 /// Decide one frame: which bodies render into a retained page, which
412 /// `DrawRetained`s composite from one and where, and which pages make room
413 /// (see the module doc). Advances the frame clock; changes nothing else
414 /// unless it refuses.
415 ///
416 /// `layers` is the frame's recorded layers, for each body's drawn bounds;
417 /// every layer enclosing a served draw has its bounds grown to hold the
418 /// draw's composite (rounded out to tiles) before any body is sized.
419 ///
420 /// # Errors
421 ///
422 /// [`EngineError::RetainedKeyMissing`] for the first `DrawRetained` whose
423 /// key is not resident at its placement — and the store is cleared.
424 pub fn plan(
425 &mut self,
426 brackets: &[RetainedBracket],
427 layers: &mut [RecordedLayer],
428 ) -> Result<RetainedPlan, EngineError> {
429 self.frame = self.frame.saturating_add(1);
430 if brackets.is_empty() {
431 // The common frame: nothing retained, nothing to decide, and no
432 // allocation spent deciding it.
433 return Ok(RetainedPlan::default());
434 }
435 let mut decisions: Vec<Option<RetainedDecision>> = vec![None; brackets.len()];
436
437 // Every draw first: one that cannot be served refuses the frame, and
438 // nothing the records below would decide matters then.
439 for (slot, bracket) in decisions.iter_mut().zip(brackets) {
440 if let RetainedKind::Draw { window, masked, .. } = bracket.kind {
441 match self.resolve_draw(bracket, window, masked) {
442 Some(decision) => *slot = Some(decision),
443 None => {
444 self.clear();
445 self.refused = true;
446 return Err(EngineError::RetainedKeyMissing { key: bracket.key });
447 }
448 }
449 }
450 }
451
452 // A served draw nested inside a recorded layer composites into that
453 // layer's page, which was sized from the draws the layer holds — and a
454 // `DrawRetained`'s own layer holds none. Every enclosing layer's bounds
455 // grow to take the composite in, before anything is sized from them.
456 grow_enclosing_layers(brackets, &decisions, layers);
457
458 // Every key the frame names is off limits to eviction: a drawn page is
459 // sampled this frame, and a recorded one is either reused or replaced.
460 let named: Vec<u64> = brackets.iter().map(|bracket| bracket.key).collect();
461 let drawn: Vec<u64> = brackets
462 .iter()
463 .filter(|bracket| bracket.is_draw())
464 .map(|bracket| bracket.key)
465 .collect();
466 let mut candidates: Vec<(u64, u64, u64)> = self
467 .pages
468 .iter()
469 .filter(|page| !named.contains(&page.key))
470 .map(|page| (page.last_used, page.key, page.bytes()))
471 .collect();
472 candidates.sort_unstable();
473 let mut evicted = 0_usize;
474 let mut projected = self.used_bytes();
475 // A key the frame draws is never re-recorded in the same frame: the
476 // draw samples the page the ledger promised, not one this frame
477 // rewrites.
478 let mut recorded: Vec<u64> = drawn;
479
480 for (index, bracket) in brackets.iter().enumerate() {
481 let RetainedKind::Record { rect, retainable } = bracket.kind else {
482 continue;
483 };
484 let decision = self.admit(
485 bracket,
486 rect,
487 retainable,
488 layers,
489 &recorded,
490 &mut projected,
491 &candidates,
492 &mut evicted,
493 );
494 if matches!(decision, RetainedDecision::Record { .. }) {
495 recorded.push(bracket.key);
496 }
497 if let Some(slot) = decisions.get_mut(index) {
498 *slot = Some(decision);
499 }
500 }
501
502 Ok(RetainedPlan {
503 decisions: decisions
504 .into_iter()
505 .map(|decision| {
506 decision.unwrap_or(RetainedDecision::Inline(RetainedRefusal::NotRetainable))
507 })
508 .collect(),
509 evictions: candidates
510 .iter()
511 .take(evicted)
512 .map(|&(_, key, _)| key)
513 .collect(),
514 })
515 }
516
517 /// Apply `plan`, decided for `brackets`, once nothing about the frame can
518 /// fail any more: evict, then give every admitted body the schedule really
519 /// rendered (`rendered`, indexed like `brackets`) its page, then bind each
520 /// bracket to the page it uses this frame.
521 ///
522 /// The evictions are the plan's least-recently-used ones plus every key
523 /// the frame re-recorded without its page receiving that recording (see
524 /// the module doc's *A page holds this frame's recording, or goes*): a
525 /// body the plan refused, an admitted body the schedule did not render,
526 /// and every key in `superseded` — the `PushRetained` brackets the compiler
527 /// swallowed into an enclosing one
528 /// ([`CompiledFrame::retained_superseded`](crate::compile::CompiledFrame::retained_superseded)).
529 pub fn commit<A>(
530 &mut self,
531 allocator: &A,
532 brackets: &[RetainedBracket],
533 superseded: &[u64],
534 plan: &RetainedPlan,
535 rendered: &[bool],
536 ) where
537 A: TextureAllocator<Texture = T, View = V>,
538 {
539 let mut changed = false;
540 for key in &plan.evictions {
541 changed |= self.remove(*key);
542 }
543 for key in stale_keys(brackets, superseded, plan, rendered) {
544 changed |= self.remove(key);
545 }
546
547 let frame = self.frame;
548 for (index, (bracket, decision)) in brackets.iter().zip(&plan.decisions).enumerate() {
549 match *decision {
550 RetainedDecision::Record { bounds } => {
551 if !rendered.get(index).copied().unwrap_or(false) {
552 continue;
553 }
554 let size = (u32::from(bounds.width()), u32::from(bounds.height()));
555 match self.position(bracket.key) {
556 Some(at) if fits(self.pages[at].size, size) => {
557 let page = &mut self.pages[at];
558 let before = page.entry();
559 page.bounds = bounds;
560 page.placement = bracket.placement;
561 page.extent_px = bracket.extent_px;
562 page.last_used = frame;
563 changed |= page.entry() != before;
564 }
565 existing => {
566 if let Some(at) = existing {
567 self.pages.swap_remove(at);
568 }
569 let (texture, view) = allocator
570 .allocate_texture(&retained_page_descriptor(size.0, size.1));
571 self.pages.push(RetainedPage {
572 key: bracket.key,
573 bounds,
574 placement: bracket.placement,
575 extent_px: bracket.extent_px,
576 size,
577 last_used: frame,
578 _texture: texture,
579 view,
580 });
581 changed = true;
582 }
583 }
584 }
585 RetainedDecision::Draw { .. } | RetainedDecision::Hidden => {
586 if let Some(at) = self.position(bracket.key) {
587 self.pages[at].last_used = frame;
588 }
589 }
590 RetainedDecision::Inline(_) => {}
591 }
592 }
593
594 self.bound.clear();
595 for (index, (bracket, decision)) in brackets.iter().zip(&plan.decisions).enumerate() {
596 let uses_page = match decision {
597 RetainedDecision::Record { .. } => rendered.get(index).copied().unwrap_or(false),
598 RetainedDecision::Draw { .. } => true,
599 RetainedDecision::Hidden | RetainedDecision::Inline(_) => false,
600 };
601 self.bound
602 .push(uses_page.then(|| self.position(bracket.key)).flatten());
603 }
604
605 if changed {
606 self.generation = self.generation.wrapping_add(1);
607 }
608 }
609
610 /// Where a `DrawRetained` composites from, or `None` when the key is not
611 /// resident at its placement.
612 fn resolve_draw(
613 &self,
614 bracket: &RetainedBracket,
615 window: RectU16,
616 masked: bool,
617 ) -> Option<RetainedDecision> {
618 if masked {
619 return None;
620 }
621 let page = self.pages.get(self.position(bracket.key)?)?;
622 if page.extent_px != bracket.extent_px {
623 return None;
624 }
625 let (dx, dy) = page.placement.offset_to(&bracket.placement)?;
626
627 // The page's bounds moved by the draw's offset, in a signed space wide
628 // enough that a move off the grid's edge is representable.
629 let moved = (
630 i64::from(page.bounds.x0) + i64::from(dx),
631 i64::from(page.bounds.y0) + i64::from(dy),
632 i64::from(page.bounds.x1) + i64::from(dx),
633 i64::from(page.bounds.y1) + i64::from(dy),
634 );
635 let x0 = moved.0.max(i64::from(window.x0));
636 let y0 = moved.1.max(i64::from(window.y0));
637 let x1 = moved.2.min(i64::from(window.x1));
638 let y1 = moved.3.min(i64::from(window.y1));
639 if x0 >= x1 || y0 >= y1 {
640 return Some(RetainedDecision::Hidden);
641 }
642 let grid = |value: i64| u16::try_from(value).ok();
643 Some(RetainedDecision::Draw {
644 dest: RectU16::new(grid(x0)?, grid(y0)?, grid(x1)?, grid(y1)?),
645 origin: (grid(x0 - moved.0)?, grid(y0 - moved.1)?),
646 })
647 }
648
649 /// Admit one body, or say why not. `projected` is the store's size once
650 /// everything admitted so far is committed; `candidates` the evictable
651 /// pages, least recently used first, of which the first `evicted` are
652 /// already planned to go.
653 #[expect(
654 clippy::too_many_arguments,
655 reason = "the frame-wide running state of `plan`'s admission loop, \
656 threaded rather than bundled so each piece stays a plain local there"
657 )]
658 fn admit(
659 &self,
660 bracket: &RetainedBracket,
661 rect: RectU16,
662 retainable: bool,
663 layers: &[RecordedLayer],
664 recorded: &[u64],
665 projected: &mut u64,
666 candidates: &[(u64, u64, u64)],
667 evicted: &mut usize,
668 ) -> RetainedDecision {
669 let refuse = RetainedDecision::Inline;
670 if !retainable {
671 return refuse(RetainedRefusal::NotRetainable);
672 }
673 if recorded.contains(&bracket.key) {
674 return refuse(RetainedRefusal::Duplicate);
675 }
676 let Some(layer) = layers.get(bracket.layer as usize) else {
677 return refuse(RetainedRefusal::NotRetainable);
678 };
679 let bounds = layer.bbox;
680 if bounds.is_empty() {
681 return refuse(RetainedRefusal::Empty);
682 }
683 // A recorded layer's bounds are `vello_common`'s strip bounds, which
684 // run one tile past a fill ending exactly on a tile edge; one tile of
685 // slack keeps that from reading as an overflow, and is all the slack
686 // there is — a body reaching further out than that drew outside its
687 // rectangle, where a scissor or the viewport may have cut it.
688 let slack = rect_grown_by_a_tile(rect);
689 if !(bounds.x0 >= slack.x0
690 && bounds.y0 >= slack.y0
691 && bounds.x1 <= slack.x1
692 && bounds.y1 <= slack.y1)
693 {
694 return refuse(RetainedRefusal::Overflow);
695 }
696 let size = (u32::from(bounds.width()), u32::from(bounds.height()));
697 if size.0 > self.max_dimension || size.1 > self.max_dimension {
698 return refuse(RetainedRefusal::TooLarge);
699 }
700 let need = page_bytes(size);
701 if need > self.budget.bytes {
702 return refuse(RetainedRefusal::OverBudget);
703 }
704
705 // What committing this body adds: nothing when the key's page is
706 // reused, its whole page when one is allocated — less the page it
707 // replaces.
708 let current = self.position(bracket.key).and_then(|at| self.pages.get(at));
709 let after = match current {
710 Some(page) if fits(page.size, size) => *projected,
711 Some(page) => projected.saturating_sub(page.bytes()).saturating_add(need),
712 None => projected.saturating_add(need),
713 };
714
715 // Evict, least recently used first, only as far as this body needs —
716 // and only if that is far enough.
717 let mut freed_to = after;
718 let mut cursor = *evicted;
719 while freed_to > self.budget.bytes {
720 let Some(&(_, _, bytes)) = candidates.get(cursor) else {
721 return refuse(RetainedRefusal::OverBudget);
722 };
723 freed_to = freed_to.saturating_sub(bytes);
724 cursor += 1;
725 }
726 *evicted = cursor;
727 *projected = freed_to;
728 RetainedDecision::Record { bounds }
729 }
730
731 fn position(&self, key: u64) -> Option<usize> {
732 self.pages.iter().position(|page| page.key == key)
733 }
734
735 /// Drop `key`'s page, answering whether there was one.
736 fn remove(&mut self, key: u64) -> bool {
737 match self.position(key) {
738 Some(at) => {
739 self.pages.swap_remove(at);
740 self.generation = self.generation.wrapping_add(1);
741 true
742 }
743 None => false,
744 }
745 }
746}
747
748/// The keys whose resident page committing `plan` must drop because the frame
749/// re-recorded them and the page will not hold that recording: a `Record` the
750/// plan refused ([`RetainedDecision::Inline`]), a `Record` the schedule did not
751/// render, and every `superseded` key (a bracket nested inside another, which
752/// the compiler lists no bracket for).
753///
754/// A key some bracket of the frame draws ([`RetainedDecision::Draw`] or
755/// [`RetainedDecision::Hidden`]), or records into its page and renders, is
756/// spared: the page holds what the frame composited or recorded under that
757/// key, and a drawn page is bound and sampled by this very frame. Two
758/// brackets naming one key in one frame is a key collision the widget tree
759/// is not meant to produce; the bracket the page serves is the one that
760/// stands. Deduplicated, in first-seen order.
761fn stale_keys(
762 brackets: &[RetainedBracket],
763 superseded: &[u64],
764 plan: &RetainedPlan,
765 rendered: &[bool],
766) -> Vec<u64> {
767 // Nothing here allocates on a frame that records no bracket, the common
768 // one.
769 let mut kept: Vec<u64> = Vec::new();
770 let mut stale: Vec<u64> = Vec::new();
771 for (index, (bracket, decision)) in brackets.iter().zip(&plan.decisions).enumerate() {
772 let keeps = match decision {
773 RetainedDecision::Draw { .. } | RetainedDecision::Hidden => true,
774 RetainedDecision::Record { .. } => rendered.get(index).copied().unwrap_or(false),
775 RetainedDecision::Inline(_) => false,
776 };
777 // A draw's decision is always `Draw` or `Hidden` — one the store
778 // cannot serve refuses the frame in `plan` — so only a `Record` ever
779 // lands among the stale.
780 if keeps {
781 kept.push(bracket.key);
782 } else {
783 stale.push(bracket.key);
784 }
785 }
786 stale.extend_from_slice(superseded);
787 let mut keys: Vec<u64> = Vec::new();
788 for key in stale {
789 if !kept.contains(&key) && !keys.contains(&key) {
790 keys.push(key);
791 }
792 }
793 keys
794}
795
796/// Grow every recorded layer enclosing a served `DrawRetained` to cover the
797/// composite it now holds, rounded out to whole tiles.
798///
799/// A layer's bounds are the union of what was drawn into it, folded in as each
800/// draw was recorded; a `DrawRetained`'s own layer is empty, so nothing of the
801/// composite it stands for reached its ancestors — a translucent layer holding
802/// nothing but a drawn page would read as covering no pixel and be dropped, and
803/// one holding more would be sized short of the composite. Walked through the
804/// recording's own parent links, bounded by the layer count so a malformed
805/// recording cannot loop.
806fn grow_enclosing_layers(
807 brackets: &[RetainedBracket],
808 decisions: &[Option<RetainedDecision>],
809 layers: &mut [RecordedLayer],
810) {
811 let mut parents: Option<Vec<Option<usize>>> = None;
812 for (bracket, decision) in brackets.iter().zip(decisions) {
813 let Some(RetainedDecision::Draw { dest, .. }) = decision else {
814 continue;
815 };
816 let parents = parents.get_or_insert_with(|| parent_layers(layers));
817 let grown = tile_bounds_of(*dest);
818 let mut at = parents.get(bracket.layer as usize).copied().flatten();
819 let mut steps = 0_usize;
820 while let Some(index) = at {
821 steps += 1;
822 if steps > layers.len() {
823 break;
824 }
825 if let Some(layer) = layers.get_mut(index) {
826 if layer.bbox.is_empty() {
827 layer.bbox = grown;
828 } else {
829 layer.bbox.union(grown);
830 }
831 }
832 at = parents.get(index).copied().flatten();
833 }
834 }
835}
836
837/// Each recorded layer's enclosing layer, or `None` for one under the frame
838/// root.
839fn parent_layers(layers: &[RecordedLayer]) -> Vec<Option<usize>> {
840 let mut parents = vec![None; layers.len()];
841 for (index, layer) in layers.iter().enumerate() {
842 for node in &layer.nodes {
843 if let Some(child) = node.layer
844 && let Some(slot) = parents.get_mut(child as usize)
845 {
846 *slot = Some(index);
847 }
848 }
849 }
850 parents
851}
852
853/// `rect` rounded out to whole tiles, saturating at the grid's edge.
854fn tile_bounds_of(rect: RectU16) -> RectU16 {
855 let down = |value: u16, step: u16| value - value % step;
856 let up = |value: u16, step: u16| match value % step {
857 0 => value,
858 rest => value.saturating_add(step - rest),
859 };
860 RectU16::new(
861 down(rect.x0, Tile::WIDTH),
862 down(rect.y0, Tile::HEIGHT),
863 up(rect.x1, Tile::WIDTH),
864 up(rect.y1, Tile::HEIGHT),
865 )
866}
867
868/// `rect` grown by one tile on every side, saturating at the grid's edges.
869fn rect_grown_by_a_tile(rect: RectU16) -> RectU16 {
870 RectU16::new(
871 rect.x0.saturating_sub(Tile::WIDTH),
872 rect.y0.saturating_sub(Tile::HEIGHT),
873 rect.x1.saturating_add(Tile::WIDTH),
874 rect.y1.saturating_add(Tile::HEIGHT),
875 )
876}
877
878/// The bytes a page of `size` texels costs.
879fn page_bytes(size: (u32, u32)) -> u64 {
880 u64::from(size.0)
881 .saturating_mul(u64::from(size.1))
882 .saturating_mul(RETAINED_BYTES_PER_TEXEL)
883}
884
885/// Whether a texture of `have` can hold a page of `need` at its origin.
886fn fits(have: (u32, u32), need: (u32, u32)) -> bool {
887 have.0 >= need.0 && have.1 >= need.1
888}