Skip to main content

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}