Skip to main content

oxframe_2d/
passes.rs

1//! A frame's draw list as a pass graph.
2//!
3//! Vector items stay in one pass. A layer (opacity on a group, a blend, a
4//! blur, a shadow, a clip) becomes a composite pass that runs after the
5//! vector pass of its contents. Shader and effect passes are not built yet.
6
7use std::hash::{Hash, Hasher};
8
9use kurbo::PathEl;
10use oxframe_core::BlendMode;
11
12use crate::draw::{DrawItem, Paint, Primitive, StrokeStyle};
13use crate::graph::NodeId;
14
15/// What a pass draws.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PassKind {
18    /// Fills, strokes and images, in paint order, into the current target.
19    Vector,
20    /// A layer composited over the target after its contents.
21    Composite,
22}
23
24impl PassKind {
25    /// `"vector"` or `"composite"`.
26    pub fn name(self) -> &'static str {
27        match self {
28            Self::Vector => "vector",
29            Self::Composite => "composite",
30        }
31    }
32}
33
34/// One pass in topological order: contents before the composite that uses them.
35#[derive(Debug, Clone, PartialEq)]
36pub struct Pass {
37    /// Vector or composite.
38    pub kind: PassKind,
39    /// Node that opened the layer. The root vector pass uses the view.
40    pub node: NodeId,
41    /// Draw items this pass owns. A composite counts the items inside it.
42    pub items: u32,
43    /// Nesting. 0 is the frame.
44    pub depth: u32,
45    /// Layer opacity. 1 for a vector pass.
46    pub opacity: f64,
47    /// Layer blend. `"normal"` for a vector pass.
48    pub blend: &'static str,
49    /// The layer blurs, clips or casts a shadow.
50    pub filtered: bool,
51}
52
53/// Passes for `items`, in the order they must run.
54///
55/// A list with no layer is exactly one vector pass.
56pub fn pass_graph(items: &[DrawItem]) -> Vec<Pass> {
57    let mut stack = vec![Scope::frame()];
58    for item in items {
59        match &item.prim {
60            Primitive::PushLayer(layer) => {
61                flush(&mut stack);
62                stack.push(Scope {
63                    node: item.node,
64                    items: 0,
65                    run: 0,
66                    depth: stack.len() as u32,
67                    layer: Some(LayerInfo {
68                        opacity: layer.opacity,
69                        blend: blend_name(layer.blend),
70                        filtered: layer.has_filter() || layer.clip.is_some(),
71                    }),
72                    passes: Vec::new(),
73                });
74            }
75            Primitive::PopLayer => {
76                flush(&mut stack);
77                let Some(mut scope) = stack.pop() else {
78                    continue;
79                };
80                if stack.is_empty() {
81                    stack.push(scope);
82                    continue;
83                }
84                let info = scope.layer.unwrap_or(LayerInfo {
85                    opacity: 1.0,
86                    blend: "normal",
87                    filtered: false,
88                });
89                let parent = stack.last_mut().expect("frame scope");
90                parent.items += scope.items;
91                parent.passes.append(&mut scope.passes);
92                parent.passes.push(Pass {
93                    kind: PassKind::Composite,
94                    node: scope.node,
95                    items: scope.items,
96                    depth: scope.depth - 1,
97                    opacity: info.opacity,
98                    blend: info.blend,
99                    filtered: info.filtered,
100                });
101            }
102            _ => {
103                let scope = stack.last_mut().expect("frame scope");
104                scope.items += 1;
105                scope.run += 1;
106            }
107        }
108    }
109    flush(&mut stack);
110    let mut passes = stack.pop().map(|s| s.passes).unwrap_or_default();
111    if passes.is_empty() {
112        passes.push(Pass {
113            kind: PassKind::Vector,
114            node: NodeId(0),
115            items: 0,
116            depth: 0,
117            opacity: 1.0,
118            blend: "normal",
119            filtered: false,
120        });
121    }
122    passes
123}
124
125struct LayerInfo {
126    opacity: f64,
127    blend: &'static str,
128    filtered: bool,
129}
130
131struct Scope {
132    node: NodeId,
133    items: u32,
134    run: u32,
135    depth: u32,
136    layer: Option<LayerInfo>,
137    passes: Vec<Pass>,
138}
139
140impl Scope {
141    fn frame() -> Self {
142        Self {
143            node: NodeId(0),
144            items: 0,
145            run: 0,
146            depth: 0,
147            layer: None,
148            passes: Vec::new(),
149        }
150    }
151}
152
153fn flush(stack: &mut [Scope]) {
154    let Some(scope) = stack.last_mut() else {
155        return;
156    };
157    if scope.run == 0 {
158        return;
159    }
160    let run = scope.run;
161    scope.run = 0;
162    scope.passes.push(Pass {
163        kind: PassKind::Vector,
164        node: scope.node,
165        items: run,
166        depth: scope.depth,
167        opacity: 1.0,
168        blend: "normal",
169        filtered: false,
170    });
171}
172
173fn blend_name(blend: BlendMode) -> &'static str {
174    match blend {
175        BlendMode::Normal => "normal",
176        BlendMode::Multiply => "multiply",
177        BlendMode::Screen => "screen",
178        BlendMode::Overlay => "overlay",
179        BlendMode::Darken => "darken",
180        BlendMode::Lighten => "lighten",
181        BlendMode::ColorDodge => "color_dodge",
182        BlendMode::ColorBurn => "color_burn",
183        BlendMode::HardLight => "hard_light",
184        BlendMode::SoftLight => "soft_light",
185        BlendMode::Difference => "difference",
186        BlendMode::Exclusion => "exclusion",
187        BlendMode::Hue => "hue",
188        BlendMode::Saturation => "saturation",
189        BlendMode::Color => "color",
190        BlendMode::Luminosity => "luminosity",
191    }
192}
193
194/// Identity of a layer slice, including its `PushLayer` at `start` through the
195/// matching `PopLayer`. Used to reuse a layer whose vectors did not change.
196pub fn layer_fingerprint(items: &[DrawItem], start: usize) -> u64 {
197    let mut hasher = std::collections::hash_map::DefaultHasher::new();
198    let mut depth = 0usize;
199    for item in &items[start..] {
200        item.node.0.hash(&mut hasher);
201        for c in item.transform.as_coeffs() {
202            c.to_bits().hash(&mut hasher);
203        }
204        match &item.prim {
205            Primitive::PushLayer(layer) => {
206                depth += 1;
207                hash_layer(&mut hasher, layer);
208            }
209            Primitive::PopLayer => {
210                2u8.hash(&mut hasher);
211                depth -= 1;
212                if depth == 0 {
213                    break;
214                }
215            }
216            Primitive::Fill { path, paint } => {
217                3u8.hash(&mut hasher);
218                hash_path(&mut hasher, path);
219                hash_paint(&mut hasher, paint);
220            }
221            Primitive::Stroke { path, paint, style } => {
222                4u8.hash(&mut hasher);
223                hash_path(&mut hasher, path);
224                hash_paint(&mut hasher, paint);
225                hash_stroke(&mut hasher, style);
226            }
227            Primitive::Image {
228                image,
229                rect,
230                opacity,
231            } => {
232                5u8.hash(&mut hasher);
233                image.id.hash(&mut hasher);
234                rect.x0.to_bits().hash(&mut hasher);
235                rect.y0.to_bits().hash(&mut hasher);
236                rect.x1.to_bits().hash(&mut hasher);
237                rect.y1.to_bits().hash(&mut hasher);
238                opacity.to_bits().hash(&mut hasher);
239            }
240        }
241    }
242    hasher.finish()
243}
244
245fn hash_layer(hasher: &mut impl Hasher, layer: &crate::draw::Layer) {
246    1u8.hash(hasher);
247    layer.opacity.to_bits().hash(hasher);
248    blend_name(layer.blend).hash(hasher);
249    layer.blur.to_bits().hash(hasher);
250    if let Some(path) = &layer.clip {
251        hash_path(hasher, path);
252    }
253    if let Some(shadow) = &layer.shadow {
254        hash_color(hasher, shadow.color);
255        shadow.blur.to_bits().hash(hasher);
256        shadow.offset.x.to_bits().hash(hasher);
257        shadow.offset.y.to_bits().hash(hasher);
258    }
259}
260
261fn hash_path(hasher: &mut impl Hasher, path: &kurbo::BezPath) {
262    for el in path.elements() {
263        match el {
264            PathEl::MoveTo(p) => {
265                0u8.hash(hasher);
266                hash_point(hasher, *p);
267            }
268            PathEl::LineTo(p) => {
269                1u8.hash(hasher);
270                hash_point(hasher, *p);
271            }
272            PathEl::QuadTo(a, b) => {
273                2u8.hash(hasher);
274                hash_point(hasher, *a);
275                hash_point(hasher, *b);
276            }
277            PathEl::CurveTo(a, b, c) => {
278                3u8.hash(hasher);
279                hash_point(hasher, *a);
280                hash_point(hasher, *b);
281                hash_point(hasher, *c);
282            }
283            PathEl::ClosePath => 4u8.hash(hasher),
284        }
285    }
286}
287
288fn hash_point(hasher: &mut impl Hasher, p: kurbo::Point) {
289    p.x.to_bits().hash(hasher);
290    p.y.to_bits().hash(hasher);
291}
292
293fn hash_paint(hasher: &mut impl Hasher, paint: &Paint) {
294    match paint {
295        Paint::Solid(c) => {
296            0u8.hash(hasher);
297            hash_color(hasher, *c);
298        }
299        Paint::Gradient(g) => {
300            1u8.hash(hasher);
301            match g.kind {
302                oxframe_core::GradientKind::Linear { from, to } => {
303                    0u8.hash(hasher);
304                    hash_point(hasher, from);
305                    hash_point(hasher, to);
306                }
307                oxframe_core::GradientKind::Radial { center, radius } => {
308                    1u8.hash(hasher);
309                    hash_point(hasher, center);
310                    radius.to_bits().hash(hasher);
311                }
312            }
313            for (offset, color) in &g.stops {
314                offset.to_bits().hash(hasher);
315                hash_color(hasher, *color);
316            }
317        }
318    }
319}
320
321fn hash_color(hasher: &mut impl Hasher, c: oxframe_core::Color) {
322    c.r.to_bits().hash(hasher);
323    c.g.to_bits().hash(hasher);
324    c.b.to_bits().hash(hasher);
325    c.a.to_bits().hash(hasher);
326}
327
328fn hash_stroke(hasher: &mut impl Hasher, style: &StrokeStyle) {
329    style.width.to_bits().hash(hasher);
330    cap_tag(style.cap).hash(hasher);
331    join_tag(style.join).hash(hasher);
332    style.dash_offset.to_bits().hash(hasher);
333    for d in &style.dash {
334        d.to_bits().hash(hasher);
335    }
336}
337
338fn cap_tag(cap: crate::draw::LineCap) -> u8 {
339    use crate::draw::LineCap::{Butt, Round, Square};
340    match cap {
341        Butt => 0,
342        Round => 1,
343        Square => 2,
344    }
345}
346
347fn join_tag(join: crate::draw::LineJoin) -> u8 {
348    use crate::draw::LineJoin::{Bevel, Miter, Round};
349    match join {
350        Miter => 0,
351        Round => 1,
352        Bevel => 2,
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use oxframe_core::Color;
359
360    use super::*;
361    use crate::graph::SceneGraph;
362    use crate::{NodeBuilder, Rect, View};
363
364    #[test]
365    fn a_scene_without_effects_is_one_vector_pass() {
366        let graph = SceneGraph::new(80, 80);
367        graph.enter(|| {
368            View::current().add(Rect::new().size(20.0).fill(Color::WHITE));
369            View::current().add(Rect::new().size(10.0).x(15.0).fill(Color::BLACK));
370        });
371        let passes = pass_graph(&graph.draw_list());
372        assert_eq!(passes.len(), 1);
373        assert_eq!(passes[0].kind, PassKind::Vector);
374        assert!(passes[0].items >= 2);
375    }
376
377    #[test]
378    fn a_blur_is_a_composite_after_its_contents() {
379        let graph = SceneGraph::new(80, 80);
380        graph.enter(|| {
381            View::current().add(
382                Rect::new()
383                    .size(20.0)
384                    .fill(Color::WHITE)
385                    .blur(2.0)
386                    .name("soft"),
387            );
388        });
389        let passes = pass_graph(&graph.draw_list());
390        assert!(
391            passes.iter().any(|p| p.kind == PassKind::Vector),
392            "{passes:?}"
393        );
394        let composite = passes
395            .iter()
396            .find(|p| p.kind == PassKind::Composite)
397            .expect("blur opens a layer");
398        assert!(composite.filtered);
399        let vector_at = passes.iter().position(|p| p.kind == PassKind::Vector);
400        let composite_at = passes.iter().position(|p| p.kind == PassKind::Composite);
401        assert!(vector_at < composite_at);
402    }
403}