Skip to main content

frust_engine/compile/
layers.rs

1//! Opacity layers, snapshot brackets, and the bracket stack they share with
2//! clips.
3//!
4//! Three of the display list's commands open a *group* — a bracket whose
5//! matching pop has to undo exactly what it did, and inside which a hoisted
6//! clear is confined (see [`clear`](super::clear)). [`GroupStack`] is that one
7//! stack. It is deliberately one stack and not three: `frust_scene` records
8//! `PopClip`, `PopLayer` and `PopSnapshot` as distinct commands, but a widget
9//! tree can emit them unbalanced or interleaved, and three stacks would let one
10//! kind of pop lift a bracket another kind still relies on. One stack means the
11//! innermost open bracket is always what closes, whichever pop closes it.
12//!
13//! ## Opacity layers
14//!
15//! A `PushLayer` at `alpha >= 1.0` composites source-over at full opacity,
16//! which is precisely what drawing its contents into the parent does — so it
17//! lowers to nothing but its rectangle's clip, exactly as `PushClip` does
18//! (`frust_scene` documents the two as the same command with `alpha` fixed).
19//! Below full opacity the layer has to be rendered in isolation and composited
20//! as a whole, which is a page and a pass in the scheduler
21//! ([`schedule`](crate::schedule)) — so it lowers to a recorded layer *plus*
22//! that same rectangular clip.
23//!
24//! The rectangle goes through the clip stack rather than through
25//! [`LayerProps::clip_path`]: frust lowers every clip to a scissor or a
26//! coverage mask and never to an intermediate texture, and the scheduler
27//! refuses a recorded layer carrying a clip path for that exact reason. Setting
28//! both would clip the layer twice.
29//!
30//! ## Snapshot brackets
31//!
32//! The engine implements no snapshot cache. `frust_scene` states what a
33//! renderer that does not must do instead: paint the body inline, wrapped
34//! exactly as if the recorder had emitted `push_transform(scale_about(scale,
35//! rect.center()))` when `scale != 1.0` and `push_layer(rect, alpha)` when
36//! `alpha < 1.0`. [`SnapshotStack`] is that emulation.
37//!
38//! The scale is applied as a *correction* composed ahead of every subsequent
39//! command's own transform rather than by rewriting the body's commands,
40//! because the body was recorded in the ordinary composed transform space: the
41//! bracket's presentation parameters are deliberately not baked into it. The
42//! correction that reproduces `push_transform` around a body already carrying
43//! `transform` is `transform * scale_about(..) * transform.inverse()` — see
44//! [`snapshot_correction`].
45//!
46//! Only the outermost bracket is honoured, which the display list explicitly
47//! permits: an inner bracket's own `alpha` and `scale` are ignored, and its
48//! depth is tracked only so the matching pop can be identified.
49
50use kurbo::{Affine, Rect};
51use peniko::BlendMode;
52use vello_common::record::LayerProps;
53
54/// One open bracket: what its matching pop has to undo, and the device-space
55/// bounds a clear hoisted past it is confined to.
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct Group {
58    clip: bool,
59    layer: bool,
60    bounds: Rect,
61}
62
63impl Group {
64    /// Whether closing this bracket pops the clip stack.
65    #[must_use]
66    pub fn closes_clip(&self) -> bool {
67        self.clip
68    }
69
70    /// Whether closing this bracket pops a recorded layer.
71    #[must_use]
72    pub fn closes_layer(&self) -> bool {
73        self.layer
74    }
75
76    /// The bracket's device-space bounds.
77    ///
78    /// Exact for the axis-aligned transforms frust records (translate and
79    /// scale); a rotated or skewed bracket bounds conservatively, which widens
80    /// a hoisted clear's confinement rather than narrowing it.
81    #[must_use]
82    pub fn bounds(&self) -> Rect {
83        self.bounds
84    }
85}
86
87/// The compiler's stack of open clip, layer and snapshot brackets.
88///
89/// Retained across frames alongside the clip stack it mirrors —
90/// [`reset`](Self::reset) is what keeps it from carrying state between frames.
91///
92/// Every entry here has a counterpart on the clip stack, and the two are
93/// pushed and popped together: a bracket is opened by pushing both, and closed
94/// by popping both. That is the invariant that lets an unbalanced pop be
95/// ignored safely rather than underflowing either one.
96#[derive(Debug, Default)]
97pub struct GroupStack {
98    entries: Vec<Group>,
99}
100
101impl GroupStack {
102    /// An empty stack, with no bracket open.
103    #[must_use]
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    /// Drop every open bracket, keeping the buffer.
109    pub fn reset(&mut self) {
110        self.entries.clear();
111    }
112
113    /// Open a clip bracket covering `bounds`.
114    pub fn push_clip(&mut self, bounds: Rect) {
115        self.entries.push(Group {
116            clip: true,
117            layer: false,
118            bounds,
119        });
120    }
121
122    /// Open a layer bracket covering `bounds`.
123    ///
124    /// `isolated` says whether the layer was recorded as a layer of its own —
125    /// a full-opacity one lowered to its clip alone, and has nothing in the
126    /// recording for its pop to close.
127    pub fn push_layer(&mut self, bounds: Rect, isolated: bool) {
128        self.entries.push(Group {
129            clip: true,
130            layer: isolated,
131            bounds,
132        });
133    }
134
135    /// Close the innermost open bracket, or `None` when none is open.
136    pub fn pop(&mut self) -> Option<Group> {
137        self.entries.pop()
138    }
139
140    /// Whether no bracket is open — a command here is at the frame root.
141    #[must_use]
142    pub fn is_empty(&self) -> bool {
143        self.entries.is_empty()
144    }
145
146    /// How many brackets are open.
147    #[must_use]
148    pub fn depth(&self) -> usize {
149        self.entries.len()
150    }
151
152    /// The device-space bounds of every open bracket, outermost first.
153    pub fn bounds(&self) -> impl Iterator<Item = Rect> + '_ {
154        self.entries.iter().map(Group::bounds)
155    }
156}
157
158/// How a `PushLayer` bracket lowers.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum LayerLowering {
161    /// Its rectangle's clip and nothing else.
162    Clip,
163    /// A recorded layer of its own, plus that same clip.
164    Isolated,
165}
166
167/// How a layer at `alpha` lowers.
168///
169/// A non-finite `alpha` answers [`LayerLowering::Isolated`], the same as any
170/// alpha below one. It never reaches here from the frame path — the compiler's
171/// up-front walk refuses such a layer outright — and the scheduler escalates a
172/// recorded layer whose opacity is not finite, so the conservative answer is
173/// the isolating one either way.
174#[must_use]
175pub fn lower_layer(alpha: f32) -> LayerLowering {
176    if alpha >= 1.0 {
177        LayerLowering::Clip
178    } else {
179        LayerLowering::Isolated
180    }
181}
182
183/// The recorded properties an isolated opacity layer carries.
184///
185/// Everything but the opacity is left at its default deliberately: a blend
186/// mode, a mask or a clip path on a recorded layer each need a route the
187/// scheduler does not serve, and frust records none of them — the layer's own
188/// rectangle is lowered through the clip stack instead (see the module
189/// header).
190#[must_use]
191pub fn layer_props(opacity: f32) -> LayerProps {
192    LayerProps {
193        blend_mode: BlendMode::default(),
194        opacity,
195        mask: None,
196        clip_path: None,
197    }
198}
199
200/// The correction that reproduces a snapshot bracket's presentation scale.
201///
202/// The body was recorded under `transform` without the scale baked in, so
203/// scaling it about `rect`'s centre means conjugating the scale by that
204/// transform: move into the body's own space, scale about the centre there,
205/// and move back. Composed ahead of the frame root, the result applies to
206/// every command inside the bracket without any of them being rewritten.
207///
208/// Answers the identity in the two cases where there is nothing to correct: a
209/// scale of exactly one, and a `transform` whose inverse is not finite. A
210/// singular transform (a zero scale, a collapsed axis) has no usable inverse,
211/// and the conjugation would carry infinities into every subsequent command's
212/// transform; dropping the presentation scale draws the body unscaled, where
213/// propagating them would refuse a frame the rest of which is perfectly
214/// drawable.
215#[must_use]
216pub fn snapshot_correction(rect: Rect, scale: f64, transform: Affine) -> Affine {
217    if scale == 1.0 {
218        return Affine::IDENTITY;
219    }
220
221    let correction = transform * Affine::scale_about(scale, rect.center()) * transform.inverse();
222    if correction.as_coeffs().iter().all(|c| c.is_finite()) {
223        correction
224    } else {
225        Affine::IDENTITY
226    }
227}
228
229/// The snapshot bracket's inline emulation: how deep the walk is inside one,
230/// and what the outermost bracket installed.
231#[derive(Debug)]
232pub struct SnapshotStack {
233    depth: usize,
234    correction: Affine,
235    layer_bracket: Option<usize>,
236}
237
238impl Default for SnapshotStack {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244impl SnapshotStack {
245    /// A stack with no bracket open and nothing corrected.
246    #[must_use]
247    pub fn new() -> Self {
248        Self {
249            depth: 0,
250            correction: Affine::IDENTITY,
251            layer_bracket: None,
252        }
253    }
254
255    /// Drop every open bracket and its correction.
256    pub fn reset(&mut self) {
257        *self = Self::new();
258    }
259
260    /// The affine to compose ahead of the frame root for every command inside
261    /// the open bracket — the identity when none is open, or when the
262    /// outermost one asked for no scale.
263    #[must_use]
264    pub fn correction(&self) -> Affine {
265        self.correction
266    }
267
268    /// Whether a bracket is open.
269    #[must_use]
270    pub fn is_open(&self) -> bool {
271        self.depth > 0
272    }
273
274    /// Enter a bracket, returning whether it is the outermost one.
275    ///
276    /// The outermost bracket installs its presentation scale as the
277    /// correction; an inner one installs nothing, which is the display list's
278    /// own rule that only the outermost bracket needs honouring.
279    pub fn enter(&mut self, rect: Rect, scale: f64, transform: Affine) -> bool {
280        let outermost = self.depth == 0;
281        if outermost {
282            self.correction = snapshot_correction(rect, scale, transform);
283        }
284        self.depth = self.depth.saturating_add(1);
285        outermost
286    }
287
288    /// Record that the outermost bracket opened a layer group, so the matching
289    /// pop closes it.
290    ///
291    /// Separate from [`enter`](Self::enter) because opening the layer is the
292    /// compiler's to do and can be declined — a bracket whose corrected
293    /// transform does not survive composition opens none, and its pop must not
294    /// then close a bracket it never opened. `bracket` is the group stack's
295    /// depth just after the layer's own push — the handle [`leave`](Self::leave)
296    /// uses to tell whether that group is still the one it would close.
297    pub fn record_layer(&mut self, bracket: usize) {
298        self.layer_bracket = Some(bracket);
299    }
300
301    /// Leave a bracket, returning whether the outermost one just closed
302    /// *having opened a layer group* — that is, whether the caller now has a
303    /// group to close.
304    ///
305    /// A pop with no bracket open is ignored, the policy the display list
306    /// states for an unbalanced `PopSnapshot`. `open_brackets` is the group
307    /// stack's current depth: when a stray pop inside the bracket's body has
308    /// already closed the group the snapshot opened, the recorded depth is no
309    /// longer reachable and no group is reported — closing one anyway would
310    /// lift an ancestor bracket the display list still holds open.
311    pub fn leave(&mut self, open_brackets: usize) -> bool {
312        if self.depth == 0 {
313            return false;
314        }
315        self.depth -= 1;
316        if self.depth > 0 {
317            return false;
318        }
319
320        self.correction = Affine::IDENTITY;
321        self.layer_bracket
322            .take()
323            .is_some_and(|bracket| bracket <= open_brackets)
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn a_full_opacity_layer_lowers_to_its_clip_alone() {
333        assert_eq!(lower_layer(1.0), LayerLowering::Clip);
334        assert_eq!(lower_layer(2.0), LayerLowering::Clip);
335        assert_eq!(lower_layer(0.5), LayerLowering::Isolated);
336        // Zero contributes nothing, but that is the scheduler's call to make
337        // from the recorded opacity, not the compiler's to make by omission.
338        assert_eq!(lower_layer(0.0), LayerLowering::Isolated);
339    }
340
341    #[test]
342    fn a_recorded_layer_carries_only_its_opacity() {
343        let props = layer_props(0.25);
344        assert_eq!(props.opacity, 0.25);
345        assert_eq!(props.blend_mode, BlendMode::default());
346        assert!(props.mask.is_none());
347        assert!(props.clip_path.is_none());
348    }
349
350    #[test]
351    fn the_innermost_bracket_is_what_closes_whichever_pop_closes_it() {
352        let mut stack = GroupStack::new();
353        stack.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
354        stack.push_layer(Rect::new(2.0, 2.0, 8.0, 8.0), true);
355        assert_eq!(stack.depth(), 2);
356
357        let inner = stack.pop().expect("two brackets are open");
358        assert!(inner.closes_clip() && inner.closes_layer());
359        let outer = stack.pop().expect("one bracket is still open");
360        assert!(outer.closes_clip() && !outer.closes_layer());
361
362        assert!(stack.is_empty());
363        assert!(stack.pop().is_none());
364    }
365
366    #[test]
367    fn a_full_opacity_layer_bracket_has_no_recorded_layer_to_close() {
368        let mut stack = GroupStack::new();
369        stack.push_layer(Rect::new(0.0, 0.0, 4.0, 4.0), false);
370        let group = stack.pop().expect("a bracket is open");
371        assert!(group.closes_clip());
372        assert!(!group.closes_layer());
373    }
374
375    #[test]
376    fn open_bracket_bounds_are_listed_outermost_first() {
377        let mut stack = GroupStack::new();
378        stack.push_clip(Rect::new(0.0, 0.0, 40.0, 40.0));
379        stack.push_layer(Rect::new(10.0, 10.0, 20.0, 20.0), true);
380
381        let bounds: Vec<Rect> = stack.bounds().collect();
382        assert_eq!(
383            bounds,
384            vec![
385                Rect::new(0.0, 0.0, 40.0, 40.0),
386                Rect::new(10.0, 10.0, 20.0, 20.0),
387            ]
388        );
389    }
390
391    #[test]
392    fn the_correction_scales_the_body_about_the_rect_centre() {
393        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
394        let correction = snapshot_correction(rect, 0.5, Affine::IDENTITY);
395
396        // The centre is the scale's fixed point; a corner moves halfway to it.
397        assert_eq!(correction * rect.center(), rect.center());
398        assert_eq!(correction * rect.origin(), (5.0, 5.0).into());
399    }
400
401    #[test]
402    fn the_correction_conjugates_by_the_body_transform() {
403        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
404        let transform = Affine::translate((100.0, 0.0));
405        let correction = snapshot_correction(rect, 0.5, transform);
406
407        // The centre in the body's own space is what stays put, so under the
408        // composed transform the body's centre is still where it was.
409        let composed = correction * transform;
410        assert_eq!(composed * rect.center(), transform * rect.center());
411    }
412
413    #[test]
414    fn a_scale_of_one_and_a_singular_transform_both_correct_nothing() {
415        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
416        assert_eq!(
417            snapshot_correction(rect, 1.0, Affine::translate((3.0, 4.0))),
418            Affine::IDENTITY
419        );
420        assert_eq!(
421            snapshot_correction(rect, 0.5, Affine::scale(0.0)),
422            Affine::IDENTITY
423        );
424    }
425
426    #[test]
427    fn only_the_outermost_bracket_installs_its_presentation() {
428        let outer = Rect::new(0.0, 0.0, 20.0, 20.0);
429        let inner = Rect::new(0.0, 0.0, 4.0, 4.0);
430        let mut stack = SnapshotStack::new();
431
432        assert!(stack.enter(outer, 0.5, Affine::IDENTITY));
433        let installed = stack.correction();
434        assert_ne!(installed, Affine::IDENTITY);
435
436        assert!(!stack.enter(inner, 4.0, Affine::IDENTITY));
437        assert_eq!(stack.correction(), installed);
438
439        // The inner pop restores nothing; the outer one restores everything.
440        assert!(!stack.leave(0));
441        assert_eq!(stack.correction(), installed);
442        assert!(!stack.leave(0));
443        assert_eq!(stack.correction(), Affine::IDENTITY);
444        assert!(!stack.is_open());
445    }
446
447    #[test]
448    fn a_bracket_that_opened_a_layer_reports_it_once_at_its_own_pop() {
449        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
450        let mut stack = SnapshotStack::new();
451
452        stack.enter(rect, 1.0, Affine::IDENTITY);
453        stack.record_layer(1);
454        stack.enter(rect, 1.0, Affine::IDENTITY);
455
456        assert!(!stack.leave(1), "the inner pop closes no group");
457        assert!(stack.leave(1), "the outer pop closes the group it opened");
458        assert!(!stack.leave(1), "an unbalanced pop closes nothing");
459    }
460
461    #[test]
462    fn an_unbalanced_pop_snapshot_is_ignored() {
463        let mut stack = SnapshotStack::new();
464        assert!(!stack.leave(0));
465        assert!(!stack.leave(0));
466        assert!(!stack.is_open());
467        assert_eq!(stack.correction(), Affine::IDENTITY);
468    }
469
470    #[test]
471    fn a_stray_pop_inside_a_snapshot_body_does_not_hand_its_group_to_pop_snapshot() {
472        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
473        let mut stack = SnapshotStack::new();
474
475        stack.enter(rect, 1.0, Affine::IDENTITY);
476        stack.record_layer(2);
477
478        // A stray pop inside the body already closed the snapshot's own group:
479        // the group stack is back below the recorded depth, so the snapshot's
480        // pop must not report a group — closing one would lift an ancestor.
481        assert!(!stack.leave(1), "a lost bracket reports no group to close");
482        assert!(!stack.leave(1), "an unbalanced pop still closes nothing");
483    }
484}