Skip to main content

valo_dl/
list.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::Arc;
3
4use valo_geometry::{FillRule, Matrix, Path, Rect};
5
6use crate::{Bounds, Image, ImageFilter, Paint, Sampling};
7
8/// `Backdrop` filters the sampled scene before a save layer's children paint.
9///
10/// Compose image filters to control the order of backdrop effects. The layer's
11/// paint separately controls how the filtered backdrop and children composite.
12#[derive(Clone, Debug, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14pub struct Backdrop {
15    /// `filter` transforms the sampled scene in local coordinates.
16    pub filter: ImageFilter,
17    /// `shared_key` lets tiles reuse the first matching filtered snapshot,
18    /// which sees the scene as of the first tile. Use one key only for tiles
19    /// over the same background with the same filter.
20    pub shared_key: Option<u64>,
21}
22
23impl Backdrop {
24    /// `new` filters the backdrop with `filter` before foreground content paints.
25    pub fn new(filter: ImageFilter) -> Self {
26        Self {
27            filter,
28            shared_key: None,
29        }
30    }
31
32    /// `blur` creates an isotropic Gaussian backdrop blur in local units.
33    pub fn blur(sigma: f32) -> Self {
34        Self::new(ImageFilter::blur(sigma, sigma))
35    }
36
37    /// `shared` marks this backdrop as one tile of a keyed group.
38    pub fn shared(mut self, key: u64) -> Self {
39        self.shared_key = Some(key);
40        self
41    }
42}
43
44/// `ClipOp` controls how a clip shape changes the current clip.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub enum ClipOp {
48    /// `Intersect` retains pixels inside the clip shape.
49    #[default]
50    Intersect,
51    /// `Difference` retains pixels outside the clip shape.
52    Difference,
53}
54
55/// `Op` is one recorded display-list command.
56///
57/// Draw and clip operations include the bounds and ordering metadata resolved
58/// by [`crate::DisplayListBuilder`] at record time. A draw's `bounds` is where
59/// it may show, list-root space, cropped by the clip: the rect replay culls it
60/// by, [`Rect::EVERYTHING`] for a draw that fills a list with no clip.
61#[derive(Clone, Debug, PartialEq)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize))]
63pub enum Op {
64    Save,
65    /// Open an offscreen layer scope, closed by the matching `Restore`. The
66    /// recorder works out every fact about the layer once and records it
67    /// here, the ones its children decide backpatched when the scope closes —
68    /// still record time; replay reads them, never works them out again.
69    SaveLayer {
70        paint: Paint,
71        /// Set = this layer is a MASK: its composite converts
72        /// the texture to COVERAGE (luminance or alpha) and multiplies the
73        /// enclosing layer by it (DstIn over the whole enclosing extent, so
74        /// content outside the mask's ink disappears).
75        mask_composite: Option<MaskKind>,
76        /// The layer's content: the union of its children's bounds,
77        /// list-root space, cropped by the bounds hint and by the clips made
78        /// inside the layer but not by the clips around it, which a filter
79        /// on the layer reads past (Flutter's layer bounds). Never padded for
80        /// the layer's own filter, nor flooded: the renderer sizes the
81        /// layer's texture from it, the clip and the paint. Unbounded when a
82        /// child fills a clip the layer does not crop (Flutter's
83        /// `content_is_unbounded`).
84        scope_bounds: Bounds,
85        /// The clip where the layer opens, cropped by the bounds hint,
86        /// list-root space: unbounded when there is neither, empty when the
87        /// hint misses the clip. Bounds the layer's texture together with
88        /// the target.
89        clip_bounds: Bounds,
90        /// Slot count when the scope opened. Children continue the SAME
91        /// depth line as the parent (Impeller's global numbering,
92        /// `Canvas::current_depth_`); the layer's own pass rebases by
93        /// subtracting it. Impeller records one span (`total_content_depth`)
94        /// and counts during replay; valo's replay never counts, so it
95        /// records both ends of the span instead.
96        base_slot: u32,
97        /// The composite draw's slot — next on the same line, after the
98        /// children's span (so the span is composite_slot - base_slot - 1).
99        composite_slot: u32,
100        /// The composite paints the layer's whole clip whatever its children
101        /// draw: a destructive blend changes the parent where the layer is
102        /// transparent, a filter that colours transparent pixels colours
103        /// them everywhere, and a backdrop layer opens full of the filtered
104        /// parent (Flutter's `content_is_unbounded`). The layer's texture
105        /// then covers its clip.
106        floods_clip: bool,
107        /// An enclosing group's alpha can ride the composite instead of a
108        /// texture of the group: the paint blends `SrcOver` with no colour
109        /// or image filter to take the alpha in before it filters.
110        takes_group_opacity: bool,
111        /// The layer's own alpha can ride its children instead: they take a
112        /// group alpha and are pairwise disjoint, and the composite is
113        /// nothing but that alpha (`takes_group_opacity`), with no backdrop
114        /// to seed and no bounds hint to crop by. Replay may then skip the
115        /// texture and draw each child at its own slot (Impeller's opacity
116        /// peephole: elision changes nothing about depth).
117        can_elide: bool,
118        /// Set = the layer opens with the filtered scene already painted
119        /// beneath it. Filter parameters are in local coordinates. Children
120        /// paint afterward, and the composite applies group alpha to the
121        /// filtered backdrop and children as one image — Flutter's
122        /// `saveLayer(bounds, paint, backdrop)`.
123        backdrop: Option<Backdrop>,
124    },
125    Restore,
126    /// Appends to the current transform (canvas semantics: applies to
127    /// subsequently drawn geometry first).
128    Transform(Matrix),
129    DrawRect {
130        rect: Rect,
131        paint: Paint,
132        bounds: Rect,
133        slot: u32,
134    },
135    DrawPath {
136        path: Arc<Path>,
137        fill_rule: FillRule,
138        paint: Paint,
139        /// The path's ink in local coordinates, stroke included and before
140        /// any effect: what a layer for the paint's effects has to hold.
141        content_bounds: Rect,
142        bounds: Rect,
143        slot: u32,
144    },
145    /// A mask-blurred solid (r)rect in CLOSED FORM — one draw, no filter
146    /// passes; why a box shadow costs one quad (Impeller's
147    /// SolidRRectBlurContents). Recorded when a solid paint has `mask_blur`;
148    /// `radii` are per corner, clockwise from top-left ([0.0; 4] = sharp).
149    RRectBlur {
150        rect: Rect,
151        radii: [f32; 4],
152        paint: Paint,
153        bounds: Rect,
154        slot: u32,
155    },
156    /// Depth-buffer clip (Impeller's "new clips"): the renderer
157    /// stencils the shape, then writes a depth CEILING at `expiry_slot` —
158    /// Intersect ceilings the exterior, Difference the interior. Draws below
159    /// the ceiling fail the depth test there; draws after the scope's restore
160    /// sit above it. Expiry is auto — restore renders nothing.
161    ClipPath {
162        path: Arc<Path>,
163        fill_rule: FillRule,
164        op: ClipOp,
165        /// The slot of the restore that ends this clip's scope (backpatched
166        /// by the builder when the scope closes — still record-time).
167        expiry_slot: u32,
168    },
169    /// Textured quad: `src` (texture px) → `dst` (local space). Sampling
170    /// picks filter/tiling; paint contributes tint (color as multiplier),
171    /// alpha, and blend.
172    DrawImage {
173        image: Image,
174        src: Rect,
175        dst: Rect,
176        sampling: Sampling,
177        paint: Paint,
178        bounds: Rect,
179        slot: u32,
180    },
181    /// Positioned glyphs from a laid-out paragraph — the TextFrame analog
182    /// (font id + glyph ids + positions, so this crate never depends on
183    /// the text stack). One op per placed run; `y` sits on the
184    /// baseline; the renderer picks bitmap/SDF/path per transform.
185    GlyphRun {
186        /// The font INSTANCE, carried by value to raster (Skia: text
187        /// blobs hold `sk_sp<SkTypeface>` — nothing is registered
188        /// renderer-side). Serialization keeps only the raster identity.
189        #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_font_uid"))]
190        font: std::sync::Arc<valo_text::Font>,
191        size: f32,
192        /// Blend/alpha/mask-blur apply like any draw; `paint.color` tints
193        /// mask glyphs (color glyphs keep their palette, alpha only).
194        paint: Paint,
195        glyphs: Arc<Vec<GlyphPos>>,
196        /// The run's ink in local coordinates, stroke included and before
197        /// any effect: what a layer for the paint's effects has to hold.
198        content_bounds: Rect,
199        bounds: Rect,
200        slot: u32,
201    },
202    /// Embed another list by reference — the retained-layer composition op.
203    DrawDisplayList {
204        list: Arc<DisplayList>,
205        bounds: Rect,
206        /// Child slots are child-relative; replay offsets them by this.
207        base_slot: u32,
208        /// The embedder judges this subtree stable and heavy enough to
209        /// raster-cache (policy is the caller's; admission stays in the
210        /// renderer).
211        cache: bool,
212    },
213}
214
215/// `MaskKind` controls how a mask layer converts pixels into coverage.
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize))]
218pub enum MaskKind {
219    /// `Luminance` derives coverage from premultiplied pixel luminance.
220    Luminance,
221    /// `Alpha` uses only the pixel alpha channel as coverage.
222    Alpha,
223}
224
225static NEXT_ID: AtomicU64 = AtomicU64::new(1);
226
227/// `DisplayList` is an immutable recording of drawing commands.
228///
229/// Display lists are GPU-free, thread-safe, and nestable. Wrap a list in
230/// [`Arc`] to share or replay it without copying its commands.
231///
232/// Two lists are equal when they record the same commands. A list is equal to itself
233/// without a comparison, and a nested list is compared the same way, so a scene that
234/// embeds the pictures its last frame did, unchanged, compares in the time of its own
235/// few commands: what a host asks before drawing a scene again.
236#[derive(Debug)]
237#[cfg_attr(feature = "serde", derive(serde::Serialize))]
238pub struct DisplayList {
239    id: u64,
240    pub(crate) ops: Vec<Op>,
241    /// Union of all draw bounds, list-root space.
242    pub(crate) bounds: Bounds,
243    /// Draw commands in this list, nested lists included.
244    pub(crate) draw_count: u32,
245    /// Depth slots consumed when replayed (draws + clip-scope restores),
246    /// nested lists included — the renderer derives its z quantum from this.
247    pub(crate) depth_slots: u32,
248    /// Per shared backdrop key: the union of the recorded regions of the
249    /// backdrop layers carrying it — the first one replayed blurs the whole
250    /// union once, and the rest reuse that blur.
251    pub(crate) backdrop_groups: Vec<BackdropGroup>,
252    /// Backdrop reads when replayed, shared or not, nested lists included.
253    /// A rasterized copy of such a list would freeze what it read.
254    pub(crate) backdrop_reads: u32,
255    /// Whether a group alpha distributes over the draws: every one
256    /// alpha-linear and none overlapping.
257    pub(crate) supports_opacity: bool,
258}
259
260/// `GlyphPos` identifies and positions one glyph within a glyph run.
261#[derive(Clone, Copy, Debug, PartialEq)]
262#[cfg_attr(feature = "serde", derive(serde::Serialize))]
263pub struct GlyphPos {
264    /// `id` is the glyph identifier in the run's font.
265    pub id: u32,
266    /// `x` is the glyph's local horizontal position in pixels.
267    pub x: f32,
268    /// `y` is the glyph's local baseline position in pixels.
269    pub y: f32,
270}
271
272/// `BackdropGroup` summarizes regions sharing one backdrop-filter key.
273#[derive(Clone, Debug)]
274#[cfg_attr(feature = "serde", derive(serde::Serialize))]
275pub struct BackdropGroup {
276    /// `key` identifies the shared backdrop group.
277    pub key: u64,
278    /// `union_bounds` encloses every region in the group.
279    pub union_bounds: Rect,
280    /// `filter` is shared when every region agrees; `None` disables reuse.
281    pub filter: Option<ImageFilter>,
282}
283
284fn next_id() -> u64 {
285    NEXT_ID.fetch_add(1, Ordering::Relaxed)
286}
287
288impl PartialEq for DisplayList {
289    fn eq(&self, other: &DisplayList) -> bool {
290        self.id == other.id || self.ops == other.ops
291    }
292}
293
294/// `Recording` is what a builder resolved about its ops while recording
295/// them, handed to the list it builds; see [`DisplayList`]'s fields.
296pub(crate) struct Recording {
297    pub bounds: Bounds,
298    pub depth_slots: u32,
299    pub supports_opacity: bool,
300}
301
302/// `Tally` is what a list's finished ops add up to, counted in one pass
303/// when the list is built: its draws, its backdrop reads and its shared
304/// backdrop groups (Impeller collects backdrop groups in a pass of its own,
305/// `FirstPassDispatcher::saveLayer`).
306#[derive(Default)]
307struct Tally {
308    draw_count: u32,
309    backdrop_reads: u32,
310    backdrop_groups: Vec<BackdropGroup>,
311}
312
313impl Tally {
314    fn of(ops: &[Op]) -> Self {
315        let mut tally = Self::default();
316        for op in ops {
317            tally.add(op);
318        }
319        tally
320    }
321
322    fn add(&mut self, op: &Op) {
323        match op {
324            Op::SaveLayer {
325                clip_bounds,
326                backdrop,
327                ..
328            } => {
329                self.draw_count += 1; // the composite
330                if let Some(backdrop) = backdrop {
331                    self.backdrop_reads += 1;
332                    self.join_backdrop_group(backdrop, clip_bounds);
333                }
334            }
335            Op::DrawDisplayList { list, .. } => {
336                self.draw_count += list.draw_count;
337                self.backdrop_reads += list.backdrop_reads;
338            }
339            Op::DrawRect { .. }
340            | Op::DrawPath { .. }
341            | Op::RRectBlur { .. }
342            | Op::DrawImage { .. }
343            | Op::GlyphRun { .. } => self.draw_count += 1,
344            Op::Save | Op::Restore | Op::Transform(_) | Op::ClipPath { .. } => {}
345        }
346    }
347
348    /// `join_backdrop_group` adds a keyed backdrop layer, whose clip is
349    /// `region`, to its group. An unclipped layer's region is every rect: a
350    /// group's union is a rect the planner cuts to its target.
351    fn join_backdrop_group(&mut self, backdrop: &Backdrop, region: &Bounds) {
352        let Some(key) = backdrop.shared_key else {
353            return;
354        };
355        let region = match region {
356            // Clipped away: the layer shows nowhere, so nothing reads its glass.
357            Bounds::Empty => return,
358            Bounds::Bounded(rect) => *rect,
359            Bounds::Unbounded => Rect::EVERYTHING,
360        };
361        match self.backdrop_groups.iter_mut().find(|g| g.key == key) {
362            Some(group) => {
363                group.union_bounds = group.union_bounds.union(&region);
364                if group.filter.as_ref() != Some(&backdrop.filter) {
365                    group.filter = None; // Different filters cannot share one snapshot.
366                }
367            }
368            None => self.backdrop_groups.push(BackdropGroup {
369                key,
370                union_bounds: region,
371                filter: Some(backdrop.filter.clone()),
372            }),
373        }
374    }
375}
376
377impl DisplayList {
378    pub(crate) fn new(ops: Vec<Op>, recording: Recording) -> Self {
379        let Recording {
380            bounds,
381            depth_slots,
382            supports_opacity,
383        } = recording;
384        let Tally {
385            draw_count,
386            backdrop_reads,
387            backdrop_groups,
388        } = Tally::of(&ops);
389        Self {
390            id: next_id(),
391            ops,
392            bounds,
393            draw_count,
394            depth_slots,
395            backdrop_groups,
396            backdrop_reads,
397            supports_opacity,
398        }
399    }
400
401    /// `id` returns the process-unique identity of this live display list.
402    ///
403    /// Deserialization creates a fresh identity; equal content does not imply
404    /// equal identity.
405    pub fn id(&self) -> u64 {
406        self.id
407    }
408
409    /// `ops` returns the recorded commands in replay order.
410    pub fn ops(&self) -> &[Op] {
411        &self.ops
412    }
413
414    /// `bounds` returns where the list's draws may show, in list
415    /// coordinates: nowhere, inside a rectangle, or, when a draw fills a clip
416    /// the list does not make, anywhere.
417    pub fn bounds(&self) -> Bounds {
418        self.bounds
419    }
420
421    /// `draw_count` returns the number of draws, including nested lists.
422    pub fn draw_count(&self) -> u32 {
423        self.draw_count
424    }
425
426    /// `depth_slots` returns the ordering slots required to replay this list.
427    pub fn depth_slots(&self) -> u32 {
428        self.depth_slots
429    }
430
431    /// `backdrop_reads` counts backdrop reads when replayed, shared or not,
432    /// nested lists included.
433    pub fn backdrop_reads(&self) -> u32 {
434        self.backdrop_reads
435    }
436
437    /// `supports_opacity` says whether a group alpha distributes over this
438    /// list's draws: every one alpha-linear and none overlapping. A layer
439    /// that embeds the list asks this instead of forfeiting elision
440    /// (Flutter's `can_apply_group_opacity`).
441    pub fn supports_opacity(&self) -> bool {
442        self.supports_opacity
443    }
444
445    /// `backdrop_group` returns the group recorded for `key`, if present.
446    pub fn backdrop_group(&self, key: u64) -> Option<&BackdropGroup> {
447        self.backdrop_groups.iter().find(|g| g.key == key)
448    }
449}
450
451#[cfg(feature = "serde")]
452fn serialize_font_uid<S: serde::Serializer>(
453    font: &std::sync::Arc<valo_text::Font>,
454    serializer: S,
455) -> Result<S::Ok, S::Error> {
456    serializer.serialize_u64(font.uid().0)
457}
458
459#[cfg(test)]
460mod equality_tests {
461    use std::sync::Arc;
462
463    use super::*;
464    use crate::{DisplayListBuilder, Paint};
465
466    fn rects(count: u32) -> DisplayList {
467        let mut b = DisplayListBuilder::new();
468        let paint = Paint::default();
469        for i in 0..count {
470            b.draw_rect(Rect::new(0.0, i as f32 * 10.0, 5.0, 5.0), &paint);
471        }
472        b.build()
473    }
474
475    #[test]
476    fn lists_with_the_same_commands_are_equal_and_others_not() {
477        assert_eq!(rects(3), rects(3));
478        assert_ne!(rects(3), rects(4));
479    }
480
481    #[test]
482    fn a_scene_over_the_same_retained_picture_is_equal_by_identity() {
483        let picture = Arc::new(rects(3));
484        let scene = |picture: &Arc<DisplayList>| {
485            let mut b = DisplayListBuilder::new();
486            b.draw_display_list(picture);
487            b.build()
488        };
489        assert_eq!(scene(&picture), scene(&picture));
490        assert_ne!(scene(&picture), scene(&Arc::new(rects(4))));
491    }
492}