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::{Image, ImageFilter, Paint, Sampling};
7
8/// `ClipOp` controls how a clip shape changes the current clip.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub enum ClipOp {
12    /// `Intersect` retains pixels inside the clip shape.
13    #[default]
14    Intersect,
15    /// `Difference` retains pixels outside the clip shape.
16    Difference,
17}
18
19/// `Op` is one recorded display-list command.
20///
21/// Draw and clip operations include the bounds and ordering metadata resolved
22/// by [`crate::DisplayListBuilder`] at record time.
23#[derive(Clone, Debug, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25pub enum Op {
26    Save,
27    /// Open an offscreen layer scope, closed by the matching `Restore`. All
28    /// oracle fields are backpatched when the scope closes — still record
29    /// time; replay reads them, never counts.
30    SaveLayer {
31        paint: Paint,
32        /// Set = this layer is a MASK: its composite converts
33        /// the texture to COVERAGE (luminance or alpha) and multiplies the
34        /// enclosing layer by it (DstIn over the whole enclosing extent, so
35        /// content outside the mask's ink disappears).
36        mask_composite: Option<MaskKind>,
37        /// Children's union bounds ∩ clip ∩ hint, list-root space — the
38        /// layer texture's size and the composite quad's rect.
39        scope_bounds: Rect,
40        /// Slot count when the scope opened. Children continue the SAME
41        /// depth line as the parent (Impeller's global numbering,
42        /// `Canvas::current_depth_`); the layer's own pass rebases by
43        /// subtracting it. Impeller records one span (`total_content_depth`)
44        /// and counts during replay; valo's replay never counts, so it
45        /// records both ends of the span instead.
46        base_slot: u32,
47        /// The composite draw's slot — next on the same line, after the
48        /// children's span (so the span is composite_slot - base_slot - 1).
49        composite_slot: u32,
50        /// Alpha-linear + pairwise-disjoint children and a plain-alpha
51        /// composite: replay may skip the texture entirely and let the
52        /// alpha ride each child at its own slot (Impeller's opacity
53        /// peephole: elision changes nothing about depth).
54        can_elide: bool,
55        /// Set = the layer opens with the filtered scene already painted
56        /// beneath it. Filter parameters are in local coordinates. Children
57        /// paint afterward, and the composite applies group alpha to the
58        /// filtered backdrop and children as one image —
59        /// Flutter's `saveLayer(bounds, paint, backdrop)`. A backdrop layer
60        /// never elides: the seed needs a texture.
61        backdrop_filter: Option<ImageFilter>,
62        /// Tiles sharing one key reuse the FIRST tile's filtered snapshot (and see the
63        /// scene as of that tile). Meaningful only with `backdrop_filter`.
64        backdrop_key: Option<u64>,
65    },
66    Restore,
67    /// Appends to the current transform (canvas semantics: applies to
68    /// subsequently drawn geometry first).
69    Transform(Matrix),
70    DrawRect {
71        rect: Rect,
72        paint: Paint,
73        bounds: Rect,
74        slot: u32,
75    },
76    DrawPath {
77        path: Arc<Path>,
78        fill_rule: FillRule,
79        paint: Paint,
80        bounds: Rect,
81        slot: u32,
82    },
83    /// A mask-blurred solid (r)rect in CLOSED FORM — one draw, no filter
84    /// passes; why a box shadow costs one quad (Impeller's
85    /// SolidRRectBlurContents). Recorded when a solid paint has `mask_blur`;
86    /// `radii` are per corner, clockwise from top-left ([0.0; 4] = sharp).
87    RRectBlur {
88        rect: Rect,
89        radii: [f32; 4],
90        paint: Paint,
91        bounds: Rect,
92        slot: u32,
93    },
94    /// Depth-buffer clip (Impeller's "new clips"): the renderer
95    /// stencils the shape, then writes a depth CEILING at `expiry_slot` —
96    /// Intersect ceilings the exterior, Difference the interior. Draws below
97    /// the ceiling fail the depth test there; draws after the scope's restore
98    /// sit above it. Expiry is auto — restore renders nothing.
99    ClipPath {
100        path: Arc<Path>,
101        fill_rule: FillRule,
102        op: ClipOp,
103        /// The slot of the restore that ends this clip's scope (backpatched
104        /// by the builder when the scope closes — still record-time).
105        expiry_slot: u32,
106    },
107    /// Textured quad: `src` (texture px) → `dst` (local space). Sampling
108    /// picks filter/tiling; paint contributes tint (color as multiplier),
109    /// alpha, and blend.
110    DrawImage {
111        image: Image,
112        src: Rect,
113        dst: Rect,
114        sampling: Sampling,
115        paint: Paint,
116        bounds: Rect,
117        slot: u32,
118    },
119    /// Positioned glyphs from a laid-out paragraph — the TextFrame analog
120    /// (font id + glyph ids + positions, so this crate never depends on
121    /// the text stack). One op per placed run; `y` sits on the
122    /// baseline; the renderer picks bitmap/SDF/path per transform.
123    GlyphRun {
124        /// The font INSTANCE, carried by value to raster (Skia: text
125        /// blobs hold `sk_sp<SkTypeface>` — nothing is registered
126        /// renderer-side). Serialization keeps only the raster identity.
127        #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_font_uid"))]
128        font: std::sync::Arc<valo_text::Font>,
129        size: f32,
130        /// Blend/alpha/mask-blur apply like any draw; `paint.color` tints
131        /// mask glyphs (color glyphs keep their palette, alpha only).
132        paint: Paint,
133        glyphs: Arc<Vec<GlyphPos>>,
134        bounds: Rect,
135        slot: u32,
136    },
137    /// Embed another list by reference — the retained-layer composition op.
138    DrawDisplayList {
139        list: Arc<DisplayList>,
140        bounds: Rect,
141        /// Child slots are child-relative; replay offsets them by this.
142        base_slot: u32,
143        /// The embedder judges this subtree stable and heavy enough to
144        /// raster-cache (policy is the caller's; admission stays in the
145        /// renderer).
146        cache: bool,
147    },
148}
149
150/// `MaskKind` controls how a mask layer converts pixels into coverage.
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize))]
153pub enum MaskKind {
154    /// `Luminance` derives coverage from premultiplied pixel luminance.
155    Luminance,
156    /// `Alpha` uses only the pixel alpha channel as coverage.
157    Alpha,
158}
159
160static NEXT_ID: AtomicU64 = AtomicU64::new(1);
161
162/// `DisplayList` is an immutable recording of drawing commands.
163///
164/// Display lists are GPU-free, thread-safe, and nestable. Wrap a list in
165/// [`Arc`] to share or replay it without copying its commands.
166///
167/// Two lists are equal when they record the same commands. A list is equal to itself
168/// without a comparison, and a nested list is compared the same way, so a scene that
169/// embeds the pictures its last frame did, unchanged, compares in the time of its own
170/// few commands: what a host asks before drawing a scene again.
171#[derive(Debug)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize))]
173pub struct DisplayList {
174    id: u64,
175    pub(crate) ops: Vec<Op>,
176    /// Union of all draw bounds, list-root space. `None` = draws nothing.
177    pub(crate) bounds: Option<Rect>,
178    /// Draw commands in this list, nested lists included.
179    pub(crate) draw_count: u32,
180    /// Depth slots consumed when replayed (draws + clip-scope restores),
181    /// nested lists included — the renderer derives its z quantum from this.
182    pub(crate) depth_slots: u32,
183    /// Per shared backdrop key: the union of the recorded regions of the
184    /// backdrop layers carrying it — the first one replayed blurs the whole
185    /// union once, and the rest reuse that blur.
186    pub(crate) backdrop_groups: Vec<BackdropGroup>,
187    /// Backdrop reads when replayed, shared or not, nested lists included.
188    /// A rasterized copy of such a list would freeze what it read.
189    pub(crate) backdrop_reads: u32,
190}
191
192/// `GlyphPos` identifies and positions one glyph within a glyph run.
193#[derive(Clone, Copy, Debug, PartialEq)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize))]
195pub struct GlyphPos {
196    /// `id` is the glyph identifier in the run's font.
197    pub id: u32,
198    /// `x` is the glyph's local horizontal position in pixels.
199    pub x: f32,
200    /// `y` is the glyph's local baseline position in pixels.
201    pub y: f32,
202}
203
204/// `BackdropGroup` summarizes regions sharing one backdrop-filter key.
205#[derive(Clone, Debug)]
206#[cfg_attr(feature = "serde", derive(serde::Serialize))]
207pub struct BackdropGroup {
208    /// `key` identifies the shared backdrop group.
209    pub key: u64,
210    /// `union_bounds` encloses every region in the group.
211    pub union_bounds: Rect,
212    /// `filter` is shared when every region agrees; `None` disables reuse.
213    pub filter: Option<ImageFilter>,
214}
215
216fn next_id() -> u64 {
217    NEXT_ID.fetch_add(1, Ordering::Relaxed)
218}
219
220impl PartialEq for DisplayList {
221    fn eq(&self, other: &DisplayList) -> bool {
222        self.id == other.id || self.ops == other.ops
223    }
224}
225
226impl DisplayList {
227    pub(crate) fn new(
228        ops: Vec<Op>,
229        bounds: Option<Rect>,
230        draw_count: u32,
231        depth_slots: u32,
232        backdrop_groups: Vec<BackdropGroup>,
233        backdrop_reads: u32,
234    ) -> Self {
235        Self {
236            id: next_id(),
237            ops,
238            bounds,
239            draw_count,
240            depth_slots,
241            backdrop_groups,
242            backdrop_reads,
243        }
244    }
245
246    /// `id` returns the process-unique identity of this live display list.
247    ///
248    /// Deserialization creates a fresh identity; equal content does not imply
249    /// equal identity.
250    pub fn id(&self) -> u64 {
251        self.id
252    }
253
254    /// `ops` returns the recorded commands in replay order.
255    pub fn ops(&self) -> &[Op] {
256        &self.ops
257    }
258
259    /// `bounds` returns the union of visible draw bounds in list coordinates.
260    ///
261    /// It returns `None` when the list draws nothing.
262    pub fn bounds(&self) -> Option<Rect> {
263        self.bounds
264    }
265
266    /// `draw_count` returns the number of draws, including nested lists.
267    pub fn draw_count(&self) -> u32 {
268        self.draw_count
269    }
270
271    /// `depth_slots` returns the ordering slots required to replay this list.
272    pub fn depth_slots(&self) -> u32 {
273        self.depth_slots
274    }
275
276    /// `backdrop_reads` counts backdrop reads when replayed, shared or not,
277    /// nested lists included.
278    pub fn backdrop_reads(&self) -> u32 {
279        self.backdrop_reads
280    }
281
282    /// `backdrop_group` returns the group recorded for `key`, if present.
283    pub fn backdrop_group(&self, key: u64) -> Option<&BackdropGroup> {
284        self.backdrop_groups.iter().find(|g| g.key == key)
285    }
286}
287
288#[cfg(feature = "serde")]
289fn serialize_font_uid<S: serde::Serializer>(
290    font: &std::sync::Arc<valo_text::Font>,
291    serializer: S,
292) -> Result<S::Ok, S::Error> {
293    serializer.serialize_u64(font.uid().0)
294}
295
296#[cfg(test)]
297mod equality_tests {
298    use std::sync::Arc;
299
300    use super::*;
301    use crate::{DisplayListBuilder, Paint};
302
303    fn rects(count: u32) -> DisplayList {
304        let mut b = DisplayListBuilder::new();
305        let paint = Paint::default();
306        for i in 0..count {
307            b.draw_rect(Rect::new(0.0, i as f32 * 10.0, 5.0, 5.0), &paint);
308        }
309        b.build()
310    }
311
312    #[test]
313    fn lists_with_the_same_commands_are_equal_and_others_not() {
314        assert_eq!(rects(3), rects(3));
315        assert_ne!(rects(3), rects(4));
316    }
317
318    #[test]
319    fn a_scene_over_the_same_retained_picture_is_equal_by_identity() {
320        let picture = Arc::new(rects(3));
321        let scene = |picture: &Arc<DisplayList>| {
322            let mut b = DisplayListBuilder::new();
323            b.draw_display_list(picture);
324            b.build()
325        };
326        assert_eq!(scene(&picture), scene(&picture));
327        assert_ne!(scene(&picture), scene(&Arc::new(rects(4))));
328    }
329}