Skip to main content

frust_core/
retained.rs

1//! Retained-page keys and the resident-key ledger.
2//!
3//! A retained page is a body the renderer rasterized once and keeps in a cache
4//! keyed by a [`RetainedKey`]. Whether a page is still resident is the
5//! renderer's knowledge, so it publishes a [`RetainedLedger`] after each frame
6//! and the shell hands the latest one to [`RenderRoot::set_retained_ledger`]
7//! before the next paint; a widget then asks
8//! [`PaintCtx::retained_available`](crate::PaintCtx::retained_available) whether
9//! to draw the page or paint the body again. The ledger is one frame stale by
10//! design: the UI thread never waits on the render thread for it.
11//!
12//! # Which recording a resident page holds
13//!
14//! Staleness has two shapes, and the renderer can repair only one of them. A
15//! page the ledger lists that the renderer has since dropped is refused at
16//! draw time (the frame presents nothing and the UI paints again against the
17//! emptied ledger). A page that is resident but holds an **older recording**
18//! than the body the widget last recorded is not detectable by the renderer at
19//! all: the scene that carried the newer recording can be dropped on its way
20//! to the renderer (the split's scene slot is latest-wins), leaving the old
21//! page standing under the same key, extent and scale.
22//!
23//! The paint serial closes that second gap. Every
24//! [`RenderRoot::paint`](crate::RenderRoot::paint) mints a serial (`1`, `2`,
25//! ... — [`PaintCtx::paint_serial`](crate::PaintCtx::paint_serial),
26//! [`PaintOutcome::paint_serial`](crate::PaintOutcome::paint_serial)); the
27//! shell carries it with the scene and hands it to the renderer before the
28//! encode, and the ledger's [`frame`](RetainedLedger::frame) is the serial of
29//! the last scene whose retained-store commit the renderer completed. A widget
30//! that recorded a page at serial `n` therefore draws it only when the ledger
31//! [covers](RetainedLedger::covers) `n` — a ledger published before that
32//! scene was committed (or after it was dropped and a later one committed
33//! instead) cannot vouch for it.
34//!
35//! [`RenderRoot::set_retained_ledger`]: crate::RenderRoot::set_retained_ledger
36
37use std::collections::HashMap;
38use std::sync::atomic::{AtomicU64, Ordering};
39
40/// Process-wide source of [`RetainedKey`]s. `Relaxed` is sufficient: the
41/// counter only produces distinct values and synchronizes nothing else.
42static NEXT_RETAINED_KEY: AtomicU64 = AtomicU64::new(0);
43
44/// The identity of one retained page. A renderer caches a page's rasterized
45/// body by key alone, so two unrelated owners sharing a key would alias each
46/// other's content; every key is minted from one process-wide counter so none
47/// can collide.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
49pub struct RetainedKey(pub u64);
50
51impl RetainedKey {
52    /// Mint a fresh key, distinct from every other key minted in the process.
53    pub fn mint() -> Self {
54        Self(NEXT_RETAINED_KEY.fetch_add(1, Ordering::Relaxed))
55    }
56
57    /// Reserve `count` consecutive keys and return the first (`first`,
58    /// `first + 1`, ...). For an owner that needs a fixed set of related keys
59    /// (a switcher's outgoing/incoming pair). `count == 0` reserves nothing.
60    pub fn mint_range(count: u64) -> Self {
61        Self(NEXT_RETAINED_KEY.fetch_add(count, Ordering::Relaxed))
62    }
63
64    /// The raw key as handed to `PaintScene::push_retained`/`draw_retained`.
65    pub fn get(self) -> u64 {
66        self.0
67    }
68}
69
70impl From<u64> for RetainedKey {
71    fn from(raw: u64) -> Self {
72        Self(raw)
73    }
74}
75
76/// What the renderer holds for one resident page.
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct RetainedEntry {
79    /// The page's rasterized extent in physical pixels.
80    pub extent_px: (u32, u32),
81    /// The device scale the page was rasterized at.
82    pub scale: f32,
83}
84
85/// The set of retained pages the renderer reported resident after a frame.
86#[derive(Debug, Clone, Default, PartialEq)]
87pub struct RetainedLedger {
88    /// Resident pages by raw [`RetainedKey`].
89    pub entries: HashMap<u64, RetainedEntry>,
90    /// The paint serial ([`PaintOutcome::paint_serial`](crate::PaintOutcome::paint_serial))
91    /// of the most recent scene whose retained-store commit the renderer
92    /// completed before publishing this ledger. A refused or failed frame does
93    /// not advance it; the empty ledger a store clear, surface destroy or
94    /// scale change publishes carries the last committed serial (`0` before
95    /// any). See [`RetainedLedger::covers`].
96    pub frame: u64,
97}
98
99impl RetainedLedger {
100    /// Tolerance when comparing the recorded and requested scale.
101    const SCALE_EPSILON: f32 = 1e-3;
102
103    /// An empty ledger: nothing is resident. What every shell starts with.
104    pub fn empty() -> Self {
105        Self::default()
106    }
107
108    /// Build a ledger from `(key, extent_px, scale)` triples, describing the
109    /// store after the scene painted at serial `frame` was committed.
110    pub fn from_entries(
111        frame: u64,
112        entries: impl IntoIterator<Item = (u64, (u32, u32), f32)>,
113    ) -> Self {
114        Self {
115            entries: entries
116                .into_iter()
117                .map(|(key, extent_px, scale)| (key, RetainedEntry { extent_px, scale }))
118                .collect(),
119            frame,
120        }
121    }
122
123    /// Whether the renderer had committed the scene painted at `serial` (or a
124    /// later one) when it published this ledger: `frame >= serial`. Serial `0`
125    /// — a paint no render root numbered — is covered by every ledger.
126    pub fn covers(&self, serial: u64) -> bool {
127        self.frame >= serial
128    }
129
130    /// Whether `key` is resident at exactly `extent_px` and (within `1e-3`)
131    /// `scale`. A page rasterized at another size or scale is not reusable.
132    pub fn is_resident(&self, key: RetainedKey, extent_px: (u32, u32), scale: f32) -> bool {
133        self.entries.get(&key.0).is_some_and(|e| {
134            e.extent_px == extent_px && (e.scale - scale).abs() <= Self::SCALE_EPSILON
135        })
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn minted_keys_are_distinct_and_ranges_are_disjoint() {
145        let a = RetainedKey::mint();
146        let b = RetainedKey::mint();
147        assert_ne!(a, b);
148        let r = RetainedKey::mint_range(2);
149        let c = RetainedKey::mint();
150        assert!(c.get() >= r.get() + 2);
151    }
152
153    #[test]
154    fn lookups_require_exact_extent_and_close_scale() {
155        let key = RetainedKey(7);
156        let ledger = RetainedLedger::from_entries(3, [(7, (100, 50), 2.0)]);
157        assert!(ledger.is_resident(key, (100, 50), 2.0));
158        assert!(ledger.is_resident(key, (100, 50), 2.0005));
159        assert!(!ledger.is_resident(key, (101, 50), 2.0));
160        assert!(!ledger.is_resident(key, (100, 50), 2.01));
161        assert!(!ledger.is_resident(RetainedKey(8), (100, 50), 2.0));
162    }
163
164    #[test]
165    fn a_ledger_covers_its_own_serial_and_every_earlier_one() {
166        let ledger = RetainedLedger::from_entries(5, [(7, (1, 1), 1.0)]);
167        assert!(ledger.covers(0));
168        assert!(ledger.covers(4));
169        assert!(ledger.covers(5));
170        assert!(!ledger.covers(6), "a later recording is not vouched for");
171        assert!(RetainedLedger::empty().covers(0));
172        assert!(!RetainedLedger::empty().covers(1));
173    }
174
175    #[test]
176    fn empty_ledger_is_never_resident() {
177        let ledger = RetainedLedger::empty();
178        assert!(!ledger.is_resident(RetainedKey(0), (0, 0), 1.0));
179        assert!(!ledger.is_resident(RetainedKey(1), (10, 10), 1.0));
180    }
181}