1use std::collections::BTreeMap;
2
3use kurbo::{Affine, BezPath, PathEl, Shape};
4use oxframe_2d::{
5 DrawItem, Layer, LineCap, LineJoin, Paint as MPaint, Primitive, RasterImage, SceneGraph,
6 StrokeStyle,
7};
8use oxframe_core::{BlendMode, Color, Gradient, GradientKind};
9use tiny_skia::{
10 FillRule, FilterQuality, GradientStop, LinearGradient, Mask, Paint, PathBuilder, Pixmap,
11 PixmapPaint, RadialGradient, SpreadMode, Stroke, StrokeDash, Transform,
12};
13
14use crate::blur::gaussian_blur;
15use crate::{Image, RenderOptions, Renderer, scene_to_pixels};
16
17#[derive(Default)]
19pub struct CpuRenderer {
20 pixmap: Option<Pixmap>,
21 images: BTreeMap<u64, Pixmap>,
22 pool: Vec<Pixmap>,
24 layers: Vec<CachedLayer>,
26 skip: u32,
28}
29
30struct CachedLayer {
31 key: u64,
32 region: Region,
33 buf: Pixmap,
34 shadow: Option<(u64, Pixmap)>,
36 seen: bool,
37}
38
39impl CpuRenderer {
40 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn render_items(&mut self, items: &[DrawItem], view: Affine, w: u32, h: u32) -> Pixmap {
48 let mut target = Pixmap::new(w.max(1), h.max(1)).expect("valid pixmap size");
49 self.draw_items(&mut target, items, view);
50 target
51 }
52
53 fn take_layer(&mut self, w: u32, h: u32) -> Pixmap {
54 let (w, h) = (w.max(1), h.max(1));
55 match self
56 .pool
57 .iter()
58 .position(|p| p.width() == w && p.height() == h)
59 {
60 Some(i) => {
61 let mut p = self.pool.swap_remove(i);
62 p.fill(tiny_skia::Color::TRANSPARENT);
63 p
64 }
65 None => Pixmap::new(w, h).expect("valid pixmap size"),
66 }
67 }
68
69 fn draw_items(&mut self, base: &mut Pixmap, items: &[DrawItem], view: Affine) {
70 let full = Region {
71 x: 0,
72 y: 0,
73 w: base.width(),
74 h: base.height(),
75 };
76 for slot in &mut self.layers {
77 slot.seen = false;
78 }
79 self.skip = 0;
80 let mut stack: Vec<(Pixmap, Region, Layer, Affine, u64, bool)> = Vec::new();
82 for (i, item) in items.iter().enumerate() {
83 let tf = view * item.transform;
84 if self.skip > 0 {
85 match &item.prim {
86 Primitive::PushLayer(_) => self.skip += 1,
87 Primitive::PopLayer => {
88 self.skip -= 1;
89 if self.skip == 0 {
90 self.pop_layer(&mut stack, base, &full);
91 }
92 }
93 _ => {}
94 }
95 continue;
96 }
97 match &item.prim {
98 Primitive::PushLayer(layer) => {
99 let parent = stack.last().map_or(full, |(_, r, _, _, _, _)| *r);
100 let region = layer_region(items, i, view, parent).unwrap_or(Region {
101 x: parent.x,
102 y: parent.y,
103 w: 1,
104 h: 1,
105 });
106 let key = cache_key(oxframe_2d::layer_fingerprint(items, i), region);
107 if let Some(slot) = self
108 .layers
109 .iter_mut()
110 .find(|slot| slot.key == key && slot.region == region)
111 {
112 slot.seen = true;
113 stack.push((slot.buf.clone(), region, layer.clone(), tf, key, true));
114 self.skip = 1;
115 } else {
116 let buf = self.take_layer(region.w, region.h);
117 stack.push((buf, region, layer.clone(), tf, key, false));
118 }
119 }
120 Primitive::PopLayer => self.pop_layer(&mut stack, base, &full),
121 prim => {
122 let (dst, region) = match stack.last_mut() {
123 Some((p, r, _, _, _, _)) => (p, *r),
124 None => (&mut *base, full),
125 };
126 draw_prim(dst, prim, region.shift() * tf, &mut self.images);
127 }
128 }
129 }
130 while !stack.is_empty() {
131 self.pop_layer(&mut stack, base, &full);
132 }
133 self.layers.retain(|slot| slot.seen);
134 self.skip = 0;
135 }
136
137 fn pop_layer(
138 &mut self,
139 stack: &mut Vec<(Pixmap, Region, Layer, Affine, u64, bool)>,
140 base: &mut Pixmap,
141 full: &Region,
142 ) {
143 let Some((buf, region, layer, ltf, key, replay)) = stack.pop() else {
144 return;
145 };
146 if !replay {
147 if let Some(slot) = self.layers.iter_mut().find(|slot| slot.key == key) {
148 slot.buf = buf.clone();
149 slot.region = region;
150 slot.shadow = None;
151 slot.seen = true;
152 } else {
153 self.layers.push(CachedLayer {
154 key,
155 region,
156 buf: buf.clone(),
157 shadow: None,
158 seen: true,
159 });
160 }
161 }
162 let shadow_key = layer.shadow.as_ref().map(|_| {
165 key ^ oxframe_2d::shadow_fingerprint(&layer).rotate_left(29) ^ scale_of(ltf).to_bits()
166 });
167 let slot = self.layers.iter_mut().find(|slot| slot.key == key);
168 let cached = match (shadow_key, slot) {
169 (Some(sk), Some(slot)) => slot.shadow.take().filter(|(k, _)| *k == sk).map(|(_, p)| p),
170 _ => None,
171 };
172 let (dst, dst_region) = match stack.last_mut() {
173 Some((p, r, _, _, _, _)) => (p, *r),
174 None => (&mut *base, *full),
175 };
176 let shadow = composite(
177 dst,
178 dst_region,
179 buf,
180 region,
181 &layer,
182 ltf,
183 cached,
184 &mut self.pool,
185 );
186 if let (Some(sk), Some(sh)) = (shadow_key, shadow) {
187 match self.layers.iter_mut().find(|slot| slot.key == key) {
188 Some(slot) => slot.shadow = Some((sk, sh)),
189 None => self.pool.push(sh),
190 }
191 }
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197struct Region {
198 x: i32,
199 y: i32,
200 w: u32,
201 h: u32,
202}
203
204impl Region {
205 fn shift(self) -> Affine {
207 Affine::translate((-f64::from(self.x), -f64::from(self.y)))
208 }
209}
210
211fn cache_key(fingerprint: u64, region: Region) -> u64 {
212 fingerprint
213 ^ (region.w as u64).wrapping_shl(1)
214 ^ (region.h as u64).wrapping_shl(17)
215 ^ (region.x as u32 as u64).wrapping_shl(33)
216 ^ (region.y as u32 as u64).wrapping_shl(49)
217}
218
219fn layer_region(items: &[DrawItem], start: usize, view: Affine, within: Region) -> Option<Region> {
222 let mut bb: Option<kurbo::Rect> = None;
223 let mut grow = 0.0f64;
224 let mut depth = 0usize;
225 for item in &items[start..] {
226 let tf = view * item.transform;
227 let r = match &item.prim {
228 Primitive::PushLayer(l) => {
229 depth += 1;
230 let k = scale_of(tf);
231 grow += 3.0 * l.blur * k;
232 if let Some(sh) = &l.shadow {
233 let off = linear(tf) * sh.offset.to_point();
234 grow += 3.0 * sh.blur * k + off.x.abs().max(off.y.abs());
235 }
236 None
237 }
238 Primitive::PopLayer => {
239 depth -= 1;
240 if depth == 0 {
241 break;
242 }
243 None
244 }
245 Primitive::Fill { path, .. } => Some(tf.transform_rect_bbox(path.bounding_box())),
246 Primitive::Stroke { path, style, .. } => {
247 let pad = style.width * scale_of(tf);
248 Some(
249 tf.transform_rect_bbox(path.bounding_box())
250 .inflate(pad, pad),
251 )
252 }
253 Primitive::Image { rect, .. } => Some(tf.transform_rect_bbox(*rect)),
254 Primitive::ReadFrame(_) | Primitive::BeginEffect(_) | Primitive::EndEffect => None,
255 };
256 if let Some(r) = r {
257 bb = Some(bb.map_or(r, |b| b.union(r)));
258 }
259 }
260 let r = bb?.inflate(grow + 2.0, grow + 2.0);
261 let x0 = r.x0.floor().max(f64::from(within.x)) as i32;
262 let y0 = r.y0.floor().max(f64::from(within.y)) as i32;
263 let x1 = r.x1.ceil().min(f64::from(within.x) + f64::from(within.w)) as i32;
264 let y1 = r.y1.ceil().min(f64::from(within.y) + f64::from(within.h)) as i32;
265 (x1 > x0 && y1 > y0).then(|| Region {
266 x: x0,
267 y: y0,
268 w: (x1 - x0) as u32,
269 h: (y1 - y0) as u32,
270 })
271}
272
273fn scale_of(tf: Affine) -> f64 {
274 tf.determinant().abs().sqrt()
275}
276
277#[allow(clippy::too_many_arguments)]
280fn composite(
281 dst: &mut Pixmap,
282 dst_region: Region,
283 mut buf: Pixmap,
284 region: Region,
285 layer: &Layer,
286 tf: Affine,
287 cached: Option<Pixmap>,
288 pool: &mut Vec<Pixmap>,
289) -> Option<Pixmap> {
290 let (w, h) = (buf.width() as usize, buf.height() as usize);
291 let k = scale_of(tf);
292 if let Some(clip) = &layer.clip
293 && let Some(path) = to_skia_path(clip)
294 && let Some(mut mask) = Mask::new(buf.width(), buf.height())
295 {
296 mask.fill_path(
297 &path,
298 FillRule::Winding,
299 true,
300 to_skia_transform(region.shift() * tf),
301 );
302 buf.apply_mask(&mask);
303 }
304 let (dx, dy) = (region.x - dst_region.x, region.y - dst_region.y);
306 if layer.blur > 0.0 {
307 gaussian_blur(buf.data_mut(), w, h, layer.blur * k);
308 }
309 let paint = PixmapPaint {
310 opacity: layer.opacity.clamp(0.0, 1.0) as f32,
311 blend_mode: blend_mode(layer.blend),
312 quality: FilterQuality::Nearest,
313 };
314 let mut made = None;
315 if let Some(shadow) = &layer.shadow {
316 let sh = cached.unwrap_or_else(|| {
317 let mut sh = buf.clone();
319 let [r, g, b, a] = shadow.color.to_rgba8().map(u32::from);
320 for px in sh.data_mut().chunks_exact_mut(4) {
321 let cov = u32::from(px[3]) * a / 255;
322 px[0] = (r * cov / 255) as u8;
323 px[1] = (g * cov / 255) as u8;
324 px[2] = (b * cov / 255) as u8;
325 px[3] = cov as u8;
326 }
327 gaussian_blur(sh.data_mut(), w, h, shadow.blur * k);
328 sh
329 });
330 let off = linear(tf) * shadow.offset.to_point();
331 dst.draw_pixmap(
332 dx,
333 dy,
334 sh.as_ref(),
335 &paint,
336 Transform::from_translate(off.x as f32, off.y as f32),
337 None,
338 );
339 made = Some(sh);
340 }
341 dst.draw_pixmap(dx, dy, buf.as_ref(), &paint, Transform::identity(), None);
342 pool.push(buf);
343 made
344}
345
346fn linear(tf: Affine) -> Affine {
348 let [a, b, c, d, _, _] = tf.as_coeffs();
349 Affine::new([a, b, c, d, 0.0, 0.0])
350}
351
352pub(crate) fn blend_mode(b: BlendMode) -> tiny_skia::BlendMode {
353 use tiny_skia::BlendMode as T;
354 match b {
355 BlendMode::Normal => T::SourceOver,
356 BlendMode::Multiply => T::Multiply,
357 BlendMode::Screen => T::Screen,
358 BlendMode::Overlay => T::Overlay,
359 BlendMode::Darken => T::Darken,
360 BlendMode::Lighten => T::Lighten,
361 BlendMode::ColorDodge => T::ColorDodge,
362 BlendMode::ColorBurn => T::ColorBurn,
363 BlendMode::HardLight => T::HardLight,
364 BlendMode::SoftLight => T::SoftLight,
365 BlendMode::Difference => T::Difference,
366 BlendMode::Exclusion => T::Exclusion,
367 BlendMode::Hue => T::Hue,
368 BlendMode::Saturation => T::Saturation,
369 BlendMode::Color => T::Color,
370 BlendMode::Luminosity => T::Luminosity,
371 }
372}
373
374fn draw_prim(dst: &mut Pixmap, prim: &Primitive, tf: Affine, images: &mut BTreeMap<u64, Pixmap>) {
375 match prim {
376 Primitive::Fill { path, paint } => {
377 if let (Some(path), Some(p)) = (to_skia_path(path), skia_paint(paint)) {
378 dst.fill_path(&path, &p, FillRule::Winding, to_skia_transform(tf), None);
379 }
380 }
381 Primitive::Stroke { path, paint, style } => {
382 if let (Some(path), Some(p)) = (to_skia_path(path), skia_paint(paint)) {
383 dst.stroke_path(&path, &p, &skia_stroke(style), to_skia_transform(tf), None);
384 }
385 }
386 Primitive::Image {
387 image,
388 rect,
389 opacity,
390 } => {
391 let src = match images.entry(image.id) {
392 std::collections::btree_map::Entry::Occupied(e) => e.into_mut(),
393 std::collections::btree_map::Entry::Vacant(e) => {
394 let Some(pm) = to_pixmap(image) else { return };
395 e.insert(pm)
396 }
397 };
398 let place = tf
399 * Affine::translate((rect.x0, rect.y0))
400 * Affine::scale_non_uniform(
401 rect.width() / f64::from(image.width),
402 rect.height() / f64::from(image.height),
403 );
404 let paint = PixmapPaint {
405 opacity: *opacity as f32,
406 quality: FilterQuality::Bicubic,
407 ..Default::default()
408 };
409 dst.draw_pixmap(0, 0, src.as_ref(), &paint, to_skia_transform(place), None);
410 }
411 Primitive::PushLayer(_)
412 | Primitive::PopLayer
413 | Primitive::ReadFrame(_)
414 | Primitive::BeginEffect(_)
415 | Primitive::EndEffect => {}
416 }
417}
418
419pub(crate) fn to_skia_path(path: &BezPath) -> Option<tiny_skia::Path> {
420 let mut pb = PathBuilder::new();
421 for el in path.elements() {
422 match *el {
423 PathEl::MoveTo(p) => pb.move_to(p.x as f32, p.y as f32),
424 PathEl::LineTo(p) => pb.line_to(p.x as f32, p.y as f32),
425 PathEl::QuadTo(a, p) => pb.quad_to(a.x as f32, a.y as f32, p.x as f32, p.y as f32),
426 PathEl::CurveTo(a, b, p) => pb.cubic_to(
427 a.x as f32, a.y as f32, b.x as f32, b.y as f32, p.x as f32, p.y as f32,
428 ),
429 PathEl::ClosePath => pb.close(),
430 }
431 }
432 pb.finish()
433}
434
435fn to_skia_transform(a: Affine) -> Transform {
436 let [a, b, c, d, e, f] = a.as_coeffs().map(|v| v as f32);
437 Transform::from_row(a, b, c, d, e, f)
438}
439
440fn skia_color(c: Color) -> Option<tiny_skia::Color> {
441 tiny_skia::Color::from_rgba(
442 c.r.clamp(0.0, 1.0),
443 c.g.clamp(0.0, 1.0),
444 c.b.clamp(0.0, 1.0),
445 c.a.clamp(0.0, 1.0),
446 )
447}
448
449fn shader(g: &Gradient) -> Option<tiny_skia::Shader<'static>> {
450 let stops: Vec<GradientStop> = g
451 .stops
452 .iter()
453 .filter_map(|(o, c)| Some(GradientStop::new(*o as f32, skia_color(*c)?)))
454 .collect();
455 let pt = |p: kurbo::Point| tiny_skia::Point::from_xy(p.x as f32, p.y as f32);
456 match g.kind {
457 GradientKind::Linear { from, to } => LinearGradient::new(
458 pt(from),
459 pt(to),
460 stops,
461 SpreadMode::Pad,
462 Transform::identity(),
463 ),
464 GradientKind::Radial { center, radius } => RadialGradient::new(
465 pt(center),
466 0.0,
467 pt(center),
468 radius.max(1e-3) as f32,
469 stops,
470 SpreadMode::Pad,
471 Transform::identity(),
472 ),
473 }
474}
475
476fn skia_paint(p: &MPaint) -> Option<Paint<'static>> {
477 let mut paint = Paint::default();
478 match p {
479 MPaint::Solid(c) => paint.set_color(skia_color(*c)?),
480 MPaint::Gradient(g) => paint.shader = shader(g)?,
481 }
482 paint.anti_alias = true;
483 Some(paint)
484}
485
486fn skia_stroke(s: &StrokeStyle) -> Stroke {
487 Stroke {
488 width: s.width as f32,
489 line_cap: match s.cap {
490 LineCap::Butt => tiny_skia::LineCap::Butt,
491 LineCap::Round => tiny_skia::LineCap::Round,
492 LineCap::Square => tiny_skia::LineCap::Square,
493 },
494 line_join: match s.join {
495 LineJoin::Miter => tiny_skia::LineJoin::Miter,
496 LineJoin::Round => tiny_skia::LineJoin::Round,
497 LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
498 },
499 dash: (s.dash.len() >= 2 && s.dash.iter().any(|d| *d > 0.0))
500 .then(|| {
501 let mut d: Vec<f32> = s.dash.iter().map(|v| *v as f32).collect();
503 if d.len() % 2 == 1 {
504 d.extend_from_within(..);
505 }
506 StrokeDash::new(d, s.dash_offset as f32)
507 })
508 .flatten(),
509 ..Default::default()
510 }
511}
512
513fn to_pixmap(img: &RasterImage) -> Option<Pixmap> {
514 let mut pm = Pixmap::new(img.width, img.height)?;
515 for (dst, src) in pm.pixels_mut().iter_mut().zip(img.rgba.chunks_exact(4)) {
516 *dst = tiny_skia::ColorU8::from_rgba(src[0], src[1], src[2], src[3]).premultiply();
517 }
518 Some(pm)
519}
520
521pub(crate) fn demultiply(pixmap: &Pixmap) -> Vec<u8> {
523 let mut data = Vec::with_capacity(pixmap.data().len());
524 for px in pixmap.pixels() {
525 let c = px.demultiply();
526 data.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
527 }
528 data
529}
530
531impl CpuRenderer {
532 pub(crate) fn keep_images(&mut self, used: &std::collections::BTreeSet<u64>) {
534 self.images.retain(|id, _| used.contains(id));
535 }
536}
537
538impl Renderer for CpuRenderer {
539 fn render(&mut self, graph: &SceneGraph, options: &RenderOptions) -> Image {
540 let items = graph.draw_list();
541 self.render_list(graph, &items, options)
542 }
543
544 fn render_list(
545 &mut self,
546 graph: &SceneGraph,
547 items: &[DrawItem],
548 options: &RenderOptions,
549 ) -> Image {
550 let (w, h) = (options.width.max(1), options.height.max(1));
551 let mut base = match self.pixmap.take() {
552 Some(p) if p.width() == w && p.height() == h => p,
553 _ => Pixmap::new(w, h).expect("valid pixmap size"),
554 };
555 let bg = options.background;
556 base.fill(skia_color(bg).unwrap_or(tiny_skia::Color::BLACK));
557 let view = scene_to_pixels(graph.size(), (w, h));
558 self.draw_items(&mut base, items, view);
559 self.keep_images(&crate::image_ids(items));
560 let data = demultiply(&base);
561 self.pixmap = Some(base);
562 Image {
563 width: w,
564 height: h,
565 data,
566 }
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use oxframe_2d::{NodeBuilder, Rect, View};
573
574 use super::*;
575
576 #[test]
577 fn draws_centered_rect() {
578 let graph = SceneGraph::new(100, 100);
579 graph.enter(|| {
580 View::current().add(
581 Rect::new()
582 .size(50.0)
583 .fill(Color::from_hex("#ff0000").unwrap()),
584 );
585 });
586 let img = CpuRenderer::new().render(
587 &graph,
588 &RenderOptions {
589 width: 100,
590 height: 100,
591 background: Color::BLACK,
592 },
593 );
594 let px = |x: usize, y: usize| &img.data[(y * 100 + x) * 4..][..4];
595 assert_eq!(px(50, 50), [255, 0, 0, 255]);
596 assert_eq!(px(10, 10), [0, 0, 0, 255]);
597 assert_eq!(px(74, 50), [255, 0, 0, 255]);
598 assert_eq!(px(76, 50), [0, 0, 0, 255]);
599 }
600
601 #[test]
602 fn a_static_blur_replays_the_same_bytes() {
603 let graph = SceneGraph::new(64, 64);
604 graph.enter(|| {
605 View::current().add(
606 Rect::new()
607 .size(24.0)
608 .fill(Color::from_hex("#ff0000").unwrap())
609 .blur(1.5),
610 );
611 });
612 let opts = RenderOptions {
613 width: 64,
614 height: 64,
615 background: Color::BLACK,
616 };
617 let mut cached = CpuRenderer::new();
618 let first = cached.render(&graph, &opts);
619 let second = cached.render(&graph, &opts);
620 let fresh = CpuRenderer::new().render(&graph, &opts);
621 assert_eq!(first.data, fresh.data);
622 assert_eq!(second.data, fresh.data);
623 }
624
625 #[test]
626 fn a_moved_blur_is_not_served_from_the_old_buffer() {
627 let opts = RenderOptions {
628 width: 64,
629 height: 64,
630 background: Color::BLACK,
631 };
632 let at = |x: f64| {
633 let graph = SceneGraph::new(64, 64);
634 graph.enter(|| {
635 View::current().add(Rect::new().size(16.0).x(x).fill(Color::WHITE).blur(1.0));
636 });
637 graph
638 };
639 let mut renderer = CpuRenderer::new();
640 let before = renderer.render(&at(0.0), &opts);
641 let after = renderer.render(&at(18.0), &opts);
642 assert_ne!(before.data, after.data);
643 }
644}