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        /// The local-space crop the layer was opened with, as recorded: what
72        /// replaying the op passes again ([`crate::DisplayListBuilder::replay`]).
73        bounds_hint: Option<Rect>,
74        /// Set = this layer is a MASK: its composite converts
75        /// the texture to COVERAGE (luminance or alpha) and multiplies the
76        /// enclosing layer by it (DstIn over the whole enclosing extent, so
77        /// content outside the mask's ink disappears).
78        mask_composite: Option<MaskKind>,
79        /// The layer's content: the union of its children's bounds,
80        /// list-root space, cropped by the bounds hint and by the clips made
81        /// inside the layer but not by the clips around it, which a filter
82        /// on the layer reads past (Flutter's layer bounds). Never padded for
83        /// the layer's own filter, nor flooded: the renderer sizes the
84        /// layer's texture from it, the clip and the paint. Unbounded when a
85        /// child fills a clip the layer does not crop (Flutter's
86        /// `content_is_unbounded`).
87        scope_bounds: Bounds,
88        /// The clip where the layer opens, cropped by the bounds hint,
89        /// list-root space: unbounded when there is neither, empty when the
90        /// hint misses the clip. Bounds the layer's texture together with
91        /// the target.
92        clip_bounds: Bounds,
93        /// Slot count when the scope opened. Children continue the SAME
94        /// depth line as the parent (Impeller's global numbering,
95        /// `Canvas::current_depth_`); the layer's own pass rebases by
96        /// subtracting it. Impeller records one span (`total_content_depth`)
97        /// and counts during replay; valo's replay never counts, so it
98        /// records both ends of the span instead.
99        base_slot: u32,
100        /// The composite draw's slot — next on the same line, after the
101        /// children's span (so the span is composite_slot - base_slot - 1).
102        composite_slot: u32,
103        /// The composite paints the layer's whole clip whatever its children
104        /// draw: a destructive blend changes the parent where the layer is
105        /// transparent, a filter that colours transparent pixels colours
106        /// them everywhere, and a backdrop layer opens full of the filtered
107        /// parent (Flutter's `content_is_unbounded`). The layer's texture
108        /// then covers its clip.
109        floods_clip: bool,
110        /// An enclosing group's alpha can ride the composite instead of a
111        /// texture of the group: the paint blends `SrcOver` with no colour
112        /// or image filter to take the alpha in before it filters.
113        takes_group_opacity: bool,
114        /// The layer's own alpha can ride its children instead: they take a
115        /// group alpha and are pairwise disjoint, and the composite is
116        /// nothing but that alpha (`takes_group_opacity`), with no backdrop
117        /// to seed and no bounds hint to crop by. Replay may then skip the
118        /// texture and draw each child at its own slot (Impeller's opacity
119        /// peephole: elision changes nothing about depth).
120        can_elide: bool,
121        /// Set = the layer opens with the filtered scene already painted
122        /// beneath it. Filter parameters are in local coordinates. Children
123        /// paint afterward, and the composite applies group alpha to the
124        /// filtered backdrop and children as one image — Flutter's
125        /// `saveLayer(bounds, paint, backdrop)`.
126        backdrop: Option<Backdrop>,
127    },
128    Restore,
129    /// Appends to the current transform (canvas semantics: applies to
130    /// subsequently drawn geometry first).
131    Transform(Matrix),
132    DrawRect {
133        rect: Rect,
134        paint: Paint,
135        bounds: Rect,
136        slot: u32,
137    },
138    DrawPath {
139        path: Arc<Path>,
140        fill_rule: FillRule,
141        paint: Paint,
142        /// The path's ink in local coordinates, stroke included and before
143        /// any effect: what a layer for the paint's effects has to hold.
144        content_bounds: Rect,
145        bounds: Rect,
146        slot: u32,
147    },
148    /// A mask-blurred solid (r)rect in CLOSED FORM — one draw, no filter
149    /// passes; why a box shadow costs one quad (Impeller's
150    /// SolidRRectBlurContents). Recorded when a solid paint has `mask_blur`;
151    /// `radii` are per corner, clockwise from top-left ([0.0; 4] = sharp).
152    RRectBlur {
153        rect: Rect,
154        radii: [f32; 4],
155        paint: Paint,
156        bounds: Rect,
157        slot: u32,
158    },
159    /// Depth-buffer clip (Impeller's "new clips"): the renderer
160    /// stencils the shape, then writes a depth CEILING at `expiry_slot` —
161    /// Intersect ceilings the exterior, Difference the interior. Draws below
162    /// the ceiling fail the depth test there; draws after the scope's restore
163    /// sit above it. Expiry is auto — restore renders nothing.
164    ClipPath {
165        path: Arc<Path>,
166        fill_rule: FillRule,
167        op: ClipOp,
168        /// The slot of the restore that ends this clip's scope (backpatched
169        /// by the builder when the scope closes — still record-time).
170        expiry_slot: u32,
171    },
172    /// Textured quad: `src` (texture px) → `dst` (local space). Sampling
173    /// picks filter/tiling; paint contributes tint (color as multiplier),
174    /// alpha, and blend.
175    DrawImage {
176        image: Image,
177        src: Rect,
178        dst: Rect,
179        sampling: Sampling,
180        paint: Paint,
181        bounds: Rect,
182        slot: u32,
183    },
184    /// Positioned glyphs from a laid-out paragraph — the TextFrame analog
185    /// (font id + glyph ids + positions, so this crate never depends on
186    /// the text stack). One op per placed run; `y` sits on the
187    /// baseline; the renderer picks bitmap/SDF/path per transform.
188    GlyphRun {
189        font: RunFont,
190        /// Blend/alpha/mask-blur apply like any draw; `paint.color` tints
191        /// mask glyphs (color glyphs keep their palette, alpha only).
192        paint: Paint,
193        glyphs: Arc<Vec<GlyphPos>>,
194        /// The text the glyphs show, when the run has it.
195        text: Option<RunText>,
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/// `RunFont` is what a glyph run's glyphs are drawn with, as Skia's `SkFont` is: a face at a
273/// size, with the bold and italic it fakes.
274#[derive(Clone, Debug, PartialEq)]
275#[cfg_attr(feature = "serde", derive(serde::Serialize))]
276pub struct RunFont {
277    /// `face` is the font the glyphs come from, carried by value to raster (Skia's text blobs
278    /// hold `sk_sp<SkTypeface>`: nothing is registered renderer-side). Serialization keeps only
279    /// its raster identity.
280    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_font_uid"))]
281    pub face: Arc<valo_text::Font>,
282    /// `size` is the font size in pixels.
283    pub size: f32,
284    /// `synthesis` is the bold and italic the glyphs fake because the face lacks them;
285    /// [`valo_text::FontSynthesis::NONE`] draws the face as it is.
286    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_synthesis"))]
287    pub synthesis: valo_text::FontSynthesis,
288}
289
290/// `RunText` is the text a glyph run shows, as Skia's text blobs keep it (`allocRunText`): what a
291/// PDF reads back for selection and search. Drawing never reads it.
292#[derive(Clone, Debug, PartialEq)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize))]
294pub struct RunText {
295    /// `text` is the run's text.
296    pub text: Arc<str>,
297    /// `clusters` holds one entry for each glyph: where the part of `text` it shows starts, in
298    /// UTF-8 bytes. Glyphs sharing a cluster show it together; a ligature is one glyph whose part
299    /// runs to the next cluster.
300    pub clusters: Arc<[u32]>,
301}
302
303/// `BackdropGroup` summarizes regions sharing one backdrop-filter key.
304#[derive(Clone, Debug)]
305#[cfg_attr(feature = "serde", derive(serde::Serialize))]
306pub struct BackdropGroup {
307    /// `key` identifies the shared backdrop group.
308    pub key: u64,
309    /// `union_bounds` encloses every region in the group.
310    pub union_bounds: Rect,
311    /// `filter` is shared when every region agrees; `None` disables reuse.
312    pub filter: Option<ImageFilter>,
313}
314
315fn next_id() -> u64 {
316    NEXT_ID.fetch_add(1, Ordering::Relaxed)
317}
318
319impl PartialEq for DisplayList {
320    fn eq(&self, other: &DisplayList) -> bool {
321        self.id == other.id || self.ops == other.ops
322    }
323}
324
325/// `Recording` is what a builder resolved about its ops while recording
326/// them, handed to the list it builds; see [`DisplayList`]'s fields.
327pub(crate) struct Recording {
328    pub bounds: Bounds,
329    pub depth_slots: u32,
330    pub supports_opacity: bool,
331}
332
333/// `Tally` is what a list's finished ops add up to, counted in one pass
334/// when the list is built: its draws, its backdrop reads and its shared
335/// backdrop groups (Impeller collects backdrop groups in a pass of its own,
336/// `FirstPassDispatcher::saveLayer`).
337#[derive(Default)]
338struct Tally {
339    draw_count: u32,
340    backdrop_reads: u32,
341    backdrop_groups: Vec<BackdropGroup>,
342}
343
344impl Tally {
345    fn of(ops: &[Op]) -> Self {
346        let mut tally = Self::default();
347        for op in ops {
348            tally.add(op);
349        }
350        tally
351    }
352
353    fn add(&mut self, op: &Op) {
354        match op {
355            Op::SaveLayer {
356                clip_bounds,
357                backdrop,
358                ..
359            } => {
360                self.draw_count += 1; // the composite
361                if let Some(backdrop) = backdrop {
362                    self.backdrop_reads += 1;
363                    self.join_backdrop_group(backdrop, clip_bounds);
364                }
365            }
366            Op::DrawDisplayList { list, .. } => {
367                self.draw_count += list.draw_count;
368                self.backdrop_reads += list.backdrop_reads;
369            }
370            Op::DrawRect { .. }
371            | Op::DrawPath { .. }
372            | Op::RRectBlur { .. }
373            | Op::DrawImage { .. }
374            | Op::GlyphRun { .. } => self.draw_count += 1,
375            Op::Save | Op::Restore | Op::Transform(_) | Op::ClipPath { .. } => {}
376        }
377    }
378
379    /// `join_backdrop_group` adds a keyed backdrop layer, whose clip is
380    /// `region`, to its group. An unclipped layer's region is every rect: a
381    /// group's union is a rect the planner cuts to its target.
382    fn join_backdrop_group(&mut self, backdrop: &Backdrop, region: &Bounds) {
383        let Some(key) = backdrop.shared_key else {
384            return;
385        };
386        let region = match region {
387            // Clipped away: the layer shows nowhere, so nothing reads its glass.
388            Bounds::Empty => return,
389            Bounds::Bounded(rect) => *rect,
390            Bounds::Unbounded => Rect::EVERYTHING,
391        };
392        match self.backdrop_groups.iter_mut().find(|g| g.key == key) {
393            Some(group) => {
394                group.union_bounds = group.union_bounds.union(&region);
395                if group.filter.as_ref() != Some(&backdrop.filter) {
396                    group.filter = None; // Different filters cannot share one snapshot.
397                }
398            }
399            None => self.backdrop_groups.push(BackdropGroup {
400                key,
401                union_bounds: region,
402                filter: Some(backdrop.filter.clone()),
403            }),
404        }
405    }
406}
407
408impl DisplayList {
409    pub(crate) fn new(ops: Vec<Op>, recording: Recording) -> Self {
410        let Recording {
411            bounds,
412            depth_slots,
413            supports_opacity,
414        } = recording;
415        let Tally {
416            draw_count,
417            backdrop_reads,
418            backdrop_groups,
419        } = Tally::of(&ops);
420        Self {
421            id: next_id(),
422            ops,
423            bounds,
424            draw_count,
425            depth_slots,
426            backdrop_groups,
427            backdrop_reads,
428            supports_opacity,
429        }
430    }
431
432    /// `id` returns the process-unique identity of this live display list.
433    ///
434    /// Deserialization creates a fresh identity; equal content does not imply
435    /// equal identity.
436    pub fn id(&self) -> u64 {
437        self.id
438    }
439
440    /// `ops` returns the recorded commands in replay order.
441    pub fn ops(&self) -> &[Op] {
442        &self.ops
443    }
444
445    /// `bounds` returns where the list's draws may show, in list
446    /// coordinates: nowhere, inside a rectangle, or, when a draw fills a clip
447    /// the list does not make, anywhere.
448    pub fn bounds(&self) -> Bounds {
449        self.bounds
450    }
451
452    /// `draw_count` returns the number of draws, including nested lists.
453    pub fn draw_count(&self) -> u32 {
454        self.draw_count
455    }
456
457    /// `depth_slots` returns the ordering slots required to replay this list.
458    pub fn depth_slots(&self) -> u32 {
459        self.depth_slots
460    }
461
462    /// `backdrop_reads` counts backdrop reads when replayed, shared or not,
463    /// nested lists included.
464    pub fn backdrop_reads(&self) -> u32 {
465        self.backdrop_reads
466    }
467
468    /// `supports_opacity` says whether a group alpha distributes over this
469    /// list's draws: every one alpha-linear and none overlapping. A layer
470    /// that embeds the list asks this instead of forfeiting elision
471    /// (Flutter's `can_apply_group_opacity`).
472    pub fn supports_opacity(&self) -> bool {
473        self.supports_opacity
474    }
475
476    /// `backdrop_group` returns the group recorded for `key`, if present.
477    pub fn backdrop_group(&self, key: u64) -> Option<&BackdropGroup> {
478        self.backdrop_groups.iter().find(|g| g.key == key)
479    }
480}
481
482#[cfg(feature = "serde")]
483fn serialize_font_uid<S: serde::Serializer>(
484    font: &std::sync::Arc<valo_text::Font>,
485    serializer: S,
486) -> Result<S::Ok, S::Error> {
487    serializer.serialize_u64(font.uid().0)
488}
489
490#[cfg(feature = "serde")]
491fn serialize_synthesis<S: serde::Serializer>(
492    synthesis: &valo_text::FontSynthesis,
493    serializer: S,
494) -> Result<S::Ok, S::Error> {
495    use serde::ser::SerializeStruct;
496    let mut fields = serializer.serialize_struct("FontSynthesis", 2)?;
497    fields.serialize_field("embolden", &synthesis.embolden)?;
498    fields.serialize_field("skew", &synthesis.skew)?;
499    fields.end()
500}
501
502#[cfg(test)]
503mod equality_tests {
504    use std::sync::Arc;
505
506    use super::*;
507    use crate::{DisplayListBuilder, Paint};
508
509    fn rects(count: u32) -> DisplayList {
510        let mut b = DisplayListBuilder::new();
511        let paint = Paint::default();
512        for i in 0..count {
513            b.draw_rect(Rect::new(0.0, i as f32 * 10.0, 5.0, 5.0), &paint);
514        }
515        b.build()
516    }
517
518    #[test]
519    fn lists_with_the_same_commands_are_equal_and_others_not() {
520        assert_eq!(rects(3), rects(3));
521        assert_ne!(rects(3), rects(4));
522    }
523
524    #[test]
525    fn a_scene_over_the_same_retained_picture_is_equal_by_identity() {
526        let picture = Arc::new(rects(3));
527        let scene = |picture: &Arc<DisplayList>| {
528            let mut b = DisplayListBuilder::new();
529            b.draw_display_list(picture);
530            b.build()
531        };
532        assert_eq!(scene(&picture), scene(&picture));
533        assert_ne!(scene(&picture), scene(&Arc::new(rects(4))));
534    }
535}