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, 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)]
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 pre-filled with a blur of everything
56        /// already painted beneath it (σ in local units; replay scales it
57        /// into device px). Children paint over that glass, and the
58        /// composite applies group alpha to blur + children as one image —
59        /// Flutter's `saveLayer(bounds, paint, backdrop)`. A backdrop layer
60        /// never elides: the seed needs a texture.
61        backdrop_sigma: Option<f32>,
62        /// Tiles sharing one key reuse the FIRST tile's blur (and see the
63        /// scene as of that tile). Meaningful only with `backdrop_sigma`.
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#[derive(Debug)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize))]
168pub struct DisplayList {
169    id: u64,
170    pub(crate) ops: Vec<Op>,
171    /// Union of all draw bounds, list-root space. `None` = draws nothing.
172    pub(crate) bounds: Option<Rect>,
173    /// Draw commands in this list, nested lists included.
174    pub(crate) draw_count: u32,
175    /// Depth slots consumed when replayed (draws + clip-scope restores),
176    /// nested lists included — the renderer derives its z quantum from this.
177    pub(crate) depth_slots: u32,
178    /// Per shared backdrop key: the union of the recorded regions of the
179    /// backdrop layers carrying it — the first one replayed blurs the whole
180    /// union once, and the rest reuse that blur.
181    pub(crate) backdrop_groups: Vec<BackdropGroup>,
182    /// Backdrop reads when replayed, shared or not, nested lists included.
183    /// A rasterized copy of such a list would freeze what it read.
184    pub(crate) backdrop_reads: u32,
185}
186
187/// `GlyphPos` identifies and positions one glyph within a glyph run.
188#[derive(Clone, Copy, Debug)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize))]
190pub struct GlyphPos {
191    /// `id` is the glyph identifier in the run's font.
192    pub id: u32,
193    /// `x` is the glyph's local horizontal position in pixels.
194    pub x: f32,
195    /// `y` is the glyph's local baseline position in pixels.
196    pub y: f32,
197}
198
199/// `BackdropGroup` summarizes regions sharing one backdrop-blur key.
200#[derive(Clone, Copy, Debug)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize))]
202pub struct BackdropGroup {
203    /// `key` identifies the shared backdrop group.
204    pub key: u64,
205    /// `union_bounds` encloses every region in the group.
206    pub union_bounds: Rect,
207    /// `sigma` is the shared blur radius when every region agrees.
208    ///
209    /// It is `None` when regions with this key use different radii and cannot
210    /// share one blur result.
211    pub sigma: Option<f32>,
212}
213
214fn next_id() -> u64 {
215    NEXT_ID.fetch_add(1, Ordering::Relaxed)
216}
217
218impl DisplayList {
219    pub(crate) fn new(
220        ops: Vec<Op>,
221        bounds: Option<Rect>,
222        draw_count: u32,
223        depth_slots: u32,
224        backdrop_groups: Vec<BackdropGroup>,
225        backdrop_reads: u32,
226    ) -> Self {
227        Self {
228            id: next_id(),
229            ops,
230            bounds,
231            draw_count,
232            depth_slots,
233            backdrop_groups,
234            backdrop_reads,
235        }
236    }
237
238    /// `id` returns the process-unique identity of this live display list.
239    ///
240    /// Deserialization creates a fresh identity; equal content does not imply
241    /// equal identity.
242    pub fn id(&self) -> u64 {
243        self.id
244    }
245
246    /// `ops` returns the recorded commands in replay order.
247    pub fn ops(&self) -> &[Op] {
248        &self.ops
249    }
250
251    /// `bounds` returns the union of visible draw bounds in list coordinates.
252    ///
253    /// It returns `None` when the list draws nothing.
254    pub fn bounds(&self) -> Option<Rect> {
255        self.bounds
256    }
257
258    /// `draw_count` returns the number of draws, including nested lists.
259    pub fn draw_count(&self) -> u32 {
260        self.draw_count
261    }
262
263    /// `depth_slots` returns the ordering slots required to replay this list.
264    pub fn depth_slots(&self) -> u32 {
265        self.depth_slots
266    }
267
268    /// `backdrop_reads` counts backdrop reads when replayed, shared or not,
269    /// nested lists included.
270    pub fn backdrop_reads(&self) -> u32 {
271        self.backdrop_reads
272    }
273
274    /// `backdrop_group` returns the group recorded for `key`, if present.
275    pub fn backdrop_group(&self, key: u64) -> Option<&BackdropGroup> {
276        self.backdrop_groups.iter().find(|g| g.key == key)
277    }
278}
279
280#[cfg(feature = "serde")]
281fn serialize_font_uid<S: serde::Serializer>(
282    font: &std::sync::Arc<valo_text::Font>,
283    serializer: S,
284) -> Result<S::Ok, S::Error> {
285    serializer.serialize_u64(font.uid().0)
286}