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}