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