Skip to main content

blitz_paint/
layers.rs

1use anyrender::{Filter, PaintScene};
2use kurbo::{Affine, Shape};
3use peniko::Mix;
4use std::sync::atomic::{AtomicU32, Ordering};
5use std::{cell::Cell, sync::Arc};
6
7const LAYER_LIMIT: u32 = 1024;
8
9/// Where a layer came from.
10///
11/// The first five go through `LayerManager::maybe_with_layer` and are subject
12/// to `LAYER_LIMIT`. The last three push onto the scene directly and are only
13/// counted here, which is the point of listing them: a total taken from the
14/// managed sites alone understates the scene.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LayerSite {
17    /// `clip-path` on the element.
18    ClipPath,
19    /// `opacity`, `filter` or `backdrop-filter`, clipped to the border box.
20    Effect,
21    /// The padding-box or content-box clip an overflowing element needs.
22    Overflow,
23    /// The clip around an outset box shadow.
24    OutsetShadow,
25    /// One per background image layer, pushed unconditionally.
26    BackgroundImage,
27    /// Inset box shadow. Bypasses the manager.
28    InsetShadow,
29    /// CSS `mask`. Bypasses the manager.
30    Mask,
31    /// Border painting clips. Bypass the manager.
32    Border,
33}
34
35impl LayerSite {
36    /// Every site, in the order they are reported.
37    pub const ALL: [LayerSite; 8] = [
38        LayerSite::ClipPath,
39        LayerSite::Effect,
40        LayerSite::Overflow,
41        LayerSite::OutsetShadow,
42        LayerSite::BackgroundImage,
43        LayerSite::InsetShadow,
44        LayerSite::Mask,
45        LayerSite::Border,
46    ];
47
48    /// Short name for the frame log.
49    pub fn name(self) -> &'static str {
50        match self {
51            LayerSite::ClipPath => "clip-path",
52            LayerSite::Effect => "effect",
53            LayerSite::Overflow => "overflow",
54            LayerSite::OutsetShadow => "outset-shadow",
55            LayerSite::BackgroundImage => "bg-image",
56            LayerSite::InsetShadow => "inset-shadow",
57            LayerSite::Mask => "mask",
58            LayerSite::Border => "border",
59        }
60    }
61}
62
63/// How many layers one painted scene asked for, and what it got.
64///
65/// Clip and opacity layers are the part of scene complexity a renderer pays
66/// most for: each one is a push, a pop and a region the rasteriser has to
67/// composite separately. When `render_to_texture` inside vello is the largest
68/// item in a frame, this count is the lever on it, because the submit path is
69/// not the cost and the encode is charged elsewhere.
70///
71/// `wanted` exceeding `used` is not just a performance note. It means the
72/// scene hit `LAYER_LIMIT` and clipping was silently skipped, so content that
73/// should have been cut off was drawn in full.
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
75pub struct SceneLayerCounts {
76    /// Layers the painter asked to push.
77    pub wanted: u32,
78    /// Layers actually pushed. Lower than `wanted` only past the limit.
79    pub used: u32,
80    /// Deepest nesting reached, which bounds what the rasteriser holds at once.
81    pub max_depth: u32,
82    /// Layers pushed per [`LayerSite`], indexed by [`LayerSite::ALL`].
83    ///
84    /// These count pushes, including the three sites the manager never sees, so
85    /// their sum is larger than `used` rather than a breakdown of it.
86    pub by_site: [u32; 8],
87}
88
89/// Counts from the most recently painted scene.
90///
91/// Published unconditionally, for the same reason [`blitz_shell`'s frame log]
92/// records unconditionally: a reader that has to be enabled at launch is a
93/// reader a normally started app never feeds. Three relaxed stores per painted
94/// scene, not per layer.
95///
96/// [`blitz_shell`'s frame log]: https://docs.rs/blitz-shell
97static LATEST_WANTED: AtomicU32 = AtomicU32::new(0);
98static LATEST_USED: AtomicU32 = AtomicU32::new(0);
99static LATEST_MAX_DEPTH: AtomicU32 = AtomicU32::new(0);
100#[allow(clippy::declare_interior_mutable_const)]
101const ZERO: AtomicU32 = AtomicU32::new(0);
102static LATEST_BY_SITE: [AtomicU32; 8] = [ZERO; 8];
103
104/// Read the layer counts of the last scene painted in this process.
105///
106/// The three fields are stored separately, so a read taken while a scene is
107/// being published can mix two frames. That is acceptable here and nowhere
108/// worth a lock: the numbers move slowly, and a reader sampling once a second
109/// is not trying to attribute a count to one specific frame.
110pub fn latest_scene_layers() -> SceneLayerCounts {
111    let mut by_site = [0u32; 8];
112    for (slot, counter) in by_site.iter_mut().zip(LATEST_BY_SITE.iter()) {
113        *slot = counter.load(Ordering::Relaxed);
114    }
115    SceneLayerCounts {
116        wanted: LATEST_WANTED.load(Ordering::Relaxed),
117        used: LATEST_USED.load(Ordering::Relaxed),
118        max_depth: LATEST_MAX_DEPTH.load(Ordering::Relaxed),
119        by_site,
120    }
121}
122
123#[derive(Default)]
124pub(crate) struct LayerManager {
125    layers_used: Cell<u32>,
126    layer_depth: Cell<u32>,
127    layers_wanted: Cell<u32>,
128    layer_depth_used: Cell<u32>,
129    by_site: [Cell<u32>; 8],
130}
131
132impl LayerManager {
133    /// Publish what this scene did, for [`latest_scene_layers`].
134    pub(crate) fn publish(&self) {
135        LATEST_WANTED.store(self.layers_wanted.get(), Ordering::Relaxed);
136        LATEST_USED.store(self.layers_used.get(), Ordering::Relaxed);
137        LATEST_MAX_DEPTH.store(self.layer_depth_used.get(), Ordering::Relaxed);
138        for (counter, cell) in LATEST_BY_SITE.iter().zip(self.by_site.iter()) {
139            counter.store(cell.get(), Ordering::Relaxed);
140        }
141    }
142
143    /// Record a layer pushed straight onto the scene, bypassing this manager.
144    ///
145    /// Inset shadows, masks and border clips do that. They are not subject to
146    /// `LAYER_LIMIT` and do not appear in `wanted` or `used`, so without this
147    /// they would be invisible to every reading taken here.
148    pub(crate) fn note_unmanaged(&self, site: LayerSite) {
149        self.by_site[site as usize].update(|x| x + 1);
150    }
151
152    #[allow(clippy::too_many_arguments)]
153    pub(crate) fn maybe_with_layer<S: PaintScene, F: FnOnce(&mut S)>(
154        &self,
155        scene: &mut S,
156        site: LayerSite,
157        condition: bool,
158        opacity: f32,
159        transform: Affine,
160        shape: &impl Shape,
161        filter: Option<Arc<Filter>>,
162        backdrop_filter: Option<Arc<Filter>>,
163        paint_layer: F,
164    ) {
165        let layer_used = self.maybe_push_layer(
166            scene,
167            site,
168            condition,
169            opacity,
170            transform,
171            shape,
172            filter,
173            backdrop_filter,
174        );
175        paint_layer(scene);
176        self.maybe_pop_layer(scene, layer_used);
177    }
178
179    #[allow(clippy::too_many_arguments)]
180    pub(crate) fn maybe_push_layer(
181        &self,
182        scene: &mut impl PaintScene,
183        site: LayerSite,
184        condition: bool,
185        opacity: f32,
186        transform: Affine,
187        shape: &impl Shape,
188        filter: Option<Arc<Filter>>,
189        backdrop_filter: Option<Arc<Filter>>,
190    ) -> bool {
191        if !condition {
192            return false;
193        }
194        self.layers_wanted.update(|x| x + 1);
195        self.by_site[site as usize].update(|x| x + 1);
196
197        // Check if clips are above limit
198        let layers_available = self.layers_used.get() <= LAYER_LIMIT;
199        if !layers_available {
200            return false;
201        }
202
203        // Actually push the layer
204        if opacity == 1.0 && filter.is_none() && backdrop_filter.is_none() {
205            scene.push_clip_layer(transform, shape);
206        } else {
207            scene.push_layer(
208                Mix::Normal,
209                opacity,
210                transform,
211                shape,
212                filter,
213                backdrop_filter,
214            );
215        };
216
217        // Update accounting. The high-water mark goes in its own cell: the
218        // line here used to read `layer_depth.update(|x| x.max(layer_depth.get()))`,
219        // which is a value maxed against itself, so the deepest nesting a scene
220        // reached was never actually recorded anywhere.
221        self.layers_used.update(|x| x + 1);
222        self.layer_depth.update(|x| x + 1);
223        self.layer_depth_used
224            .update(|x| x.max(self.layer_depth.get()));
225
226        true
227    }
228
229    pub(crate) fn maybe_pop_layer(&self, scene: &mut impl PaintScene, condition: bool) {
230        if condition {
231            scene.pop_layer();
232            self.layer_depth.update(|x| x - 1);
233        }
234    }
235}