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