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}