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.
240/// The outer layer's own opacity, blend, blur, clip and shadow are left out:
241/// they are applied when the layer is composited, not drawn into it, so a
242/// fading or blurred group keeps its buffer.
243pub fn layer_fingerprint(items: &[DrawItem], start: usize) -> u64 {
244    let mut hasher = std::collections::hash_map::DefaultHasher::new();
245    let mut depth = 0usize;
246    for item in &items[start..] {
247        item.node.0.hash(&mut hasher);
248        for c in item.transform.as_coeffs() {
249            c.to_bits().hash(&mut hasher);
250        }
251        match &item.prim {
252            Primitive::PushLayer(layer) => {
253                depth += 1;
254                if depth > 1 {
255                    hash_layer(&mut hasher, layer);
256                }
257            }
258            Primitive::PopLayer => {
259                2u8.hash(&mut hasher);
260                depth -= 1;
261                if depth == 0 {
262                    break;
263                }
264            }
265            Primitive::Fill { path, paint } => {
266                3u8.hash(&mut hasher);
267                hash_path(&mut hasher, path);
268                hash_paint(&mut hasher, paint);
269            }
270            Primitive::Stroke { path, paint, style } => {
271                4u8.hash(&mut hasher);
272                hash_path(&mut hasher, path);
273                hash_paint(&mut hasher, paint);
274                hash_stroke(&mut hasher, style);
275            }
276            Primitive::Image {
277                image,
278                rect,
279                opacity,
280            } => {
281                5u8.hash(&mut hasher);
282                image.id.hash(&mut hasher);
283                rect.x0.to_bits().hash(&mut hasher);
284                rect.y0.to_bits().hash(&mut hasher);
285                rect.x1.to_bits().hash(&mut hasher);
286                rect.y1.to_bits().hash(&mut hasher);
287                opacity.to_bits().hash(&mut hasher);
288            }
289            Primitive::ReadFrame(_) | Primitive::BeginEffect(_) | Primitive::EndEffect => {
290                6u8.hash(&mut hasher);
291            }
292        }
293    }
294    hasher.finish()
295}
296
297/// Identity of what a layer's shadow is made from besides its content: the
298/// clip and blur applied before it, and the shadow's color and blur (not its
299/// offset, which only moves it, nor the opacity it is drawn with).
300pub fn shadow_fingerprint(layer: &crate::draw::Layer) -> u64 {
301    let mut hasher = std::collections::hash_map::DefaultHasher::new();
302    layer.blur.to_bits().hash(&mut hasher);
303    if let Some(path) = &layer.clip {
304        hash_path(&mut hasher, path);
305    }
306    if let Some(shadow) = &layer.shadow {
307        hash_color(&mut hasher, shadow.color);
308        shadow.blur.to_bits().hash(&mut hasher);
309    }
310    hasher.finish()
311}
312
313fn hash_layer(hasher: &mut impl Hasher, layer: &crate::draw::Layer) {
314    1u8.hash(hasher);
315    layer.opacity.to_bits().hash(hasher);
316    blend_name(layer.blend).hash(hasher);
317    layer.blur.to_bits().hash(hasher);
318    if let Some(path) = &layer.clip {
319        hash_path(hasher, path);
320    }
321    if let Some(shadow) = &layer.shadow {
322        hash_color(hasher, shadow.color);
323        shadow.blur.to_bits().hash(hasher);
324        shadow.offset.x.to_bits().hash(hasher);
325        shadow.offset.y.to_bits().hash(hasher);
326    }
327}
328
329fn hash_path(hasher: &mut impl Hasher, path: &kurbo::BezPath) {
330    for el in path.elements() {
331        match el {
332            PathEl::MoveTo(p) => {
333                0u8.hash(hasher);
334                hash_point(hasher, *p);
335            }
336            PathEl::LineTo(p) => {
337                1u8.hash(hasher);
338                hash_point(hasher, *p);
339            }
340            PathEl::QuadTo(a, b) => {
341                2u8.hash(hasher);
342                hash_point(hasher, *a);
343                hash_point(hasher, *b);
344            }
345            PathEl::CurveTo(a, b, c) => {
346                3u8.hash(hasher);
347                hash_point(hasher, *a);
348                hash_point(hasher, *b);
349                hash_point(hasher, *c);
350            }
351            PathEl::ClosePath => 4u8.hash(hasher),
352        }
353    }
354}
355
356fn hash_point(hasher: &mut impl Hasher, p: kurbo::Point) {
357    p.x.to_bits().hash(hasher);
358    p.y.to_bits().hash(hasher);
359}
360
361fn hash_paint(hasher: &mut impl Hasher, paint: &Paint) {
362    match paint {
363        Paint::Solid(c) => {
364            0u8.hash(hasher);
365            hash_color(hasher, *c);
366        }
367        Paint::Gradient(g) => {
368            1u8.hash(hasher);
369            match g.kind {
370                oxframe_core::GradientKind::Linear { from, to } => {
371                    0u8.hash(hasher);
372                    hash_point(hasher, from);
373                    hash_point(hasher, to);
374                }
375                oxframe_core::GradientKind::Radial { center, radius } => {
376                    1u8.hash(hasher);
377                    hash_point(hasher, center);
378                    radius.to_bits().hash(hasher);
379                }
380            }
381            for (offset, color) in &g.stops {
382                offset.to_bits().hash(hasher);
383                hash_color(hasher, *color);
384            }
385        }
386    }
387}
388
389fn hash_color(hasher: &mut impl Hasher, c: oxframe_core::Color) {
390    c.r.to_bits().hash(hasher);
391    c.g.to_bits().hash(hasher);
392    c.b.to_bits().hash(hasher);
393    c.a.to_bits().hash(hasher);
394}
395
396fn hash_stroke(hasher: &mut impl Hasher, style: &StrokeStyle) {
397    style.width.to_bits().hash(hasher);
398    cap_tag(style.cap).hash(hasher);
399    join_tag(style.join).hash(hasher);
400    style.dash_offset.to_bits().hash(hasher);
401    for d in &style.dash {
402        d.to_bits().hash(hasher);
403    }
404}
405
406fn cap_tag(cap: crate::draw::LineCap) -> u8 {
407    use crate::draw::LineCap::{Butt, Round, Square};
408    match cap {
409        Butt => 0,
410        Round => 1,
411        Square => 2,
412    }
413}
414
415fn join_tag(join: crate::draw::LineJoin) -> u8 {
416    use crate::draw::LineJoin::{Bevel, Miter, Round};
417    match join {
418        Miter => 0,
419        Round => 1,
420        Bevel => 2,
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use oxframe_core::Color;
427
428    use super::*;
429    use crate::graph::SceneGraph;
430    use crate::{NodeBuilder, Rect, View};
431
432    #[test]
433    fn a_scene_without_effects_is_one_vector_pass() {
434        let graph = SceneGraph::new(80, 80);
435        graph.enter(|| {
436            View::current().add(Rect::new().size(20.0).fill(Color::WHITE));
437            View::current().add(Rect::new().size(10.0).x(15.0).fill(Color::BLACK));
438        });
439        let passes = pass_graph(&graph.draw_list());
440        assert_eq!(passes.len(), 1);
441        assert_eq!(passes[0].kind, PassKind::Vector);
442        assert!(passes[0].items >= 2);
443    }
444
445    #[test]
446    fn a_blur_is_a_composite_after_its_contents() {
447        let graph = SceneGraph::new(80, 80);
448        graph.enter(|| {
449            View::current().add(
450                Rect::new()
451                    .size(20.0)
452                    .fill(Color::WHITE)
453                    .blur(2.0)
454                    .name("soft"),
455            );
456        });
457        let passes = pass_graph(&graph.draw_list());
458        assert!(
459            passes.iter().any(|p| p.kind == PassKind::Vector),
460            "{passes:?}"
461        );
462        let composite = passes
463            .iter()
464            .find(|p| p.kind == PassKind::Composite)
465            .expect("blur opens a layer");
466        assert!(composite.filtered);
467        let vector_at = passes.iter().position(|p| p.kind == PassKind::Vector);
468        let composite_at = passes.iter().position(|p| p.kind == PassKind::Composite);
469        assert!(vector_at < composite_at);
470    }
471}