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(®ion);
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}