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