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}