1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum PassKind {
20 Vector,
22 Composite,
24 Effect,
27}
28
29impl PassKind {
30 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#[derive(Debug, Clone, PartialEq)]
42pub struct Pass {
43 pub kind: PassKind,
45 pub node: NodeId,
47 pub items: u32,
49 pub depth: u32,
51 pub opacity: f64,
53 pub blend: &'static str,
55 pub filtered: bool,
57 pub shader: Option<&'static str>,
59}
60
61pub 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
238pub 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}