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 };
226 if let Some(r) = r {
227 bb = Some(bb.map_or(r, |b| b.union(r)));
228 }
229 }
230 let r = bb?.inflate(grow + 2.0, grow + 2.0);
231 let x0 = r.x0.floor().max(f64::from(within.x)) as i32;
232 let y0 = r.y0.floor().max(f64::from(within.y)) as i32;
233 let x1 = r.x1.ceil().min(f64::from(within.x) + f64::from(within.w)) as i32;
234 let y1 = r.y1.ceil().min(f64::from(within.y) + f64::from(within.h)) as i32;
235 (x1 > x0 && y1 > y0).then(|| Region {
236 x: x0,
237 y: y0,
238 w: (x1 - x0) as u32,
239 h: (y1 - y0) as u32,
240 })
241}
242
243fn scale_of(tf: Affine) -> f64 {
244 tf.determinant().abs().sqrt()
245}
246
247fn composite(
248 dst: &mut Pixmap,
249 dst_region: Region,
250 mut buf: Pixmap,
251 region: Region,
252 layer: &Layer,
253 tf: Affine,
254 pool: &mut Vec<Pixmap>,
255) {
256 let (w, h) = (buf.width() as usize, buf.height() as usize);
257 let k = scale_of(tf);
258 if let Some(clip) = &layer.clip
259 && let Some(path) = to_skia_path(clip)
260 && let Some(mut mask) = Mask::new(buf.width(), buf.height())
261 {
262 mask.fill_path(
263 &path,
264 FillRule::Winding,
265 true,
266 to_skia_transform(region.shift() * tf),
267 );
268 buf.apply_mask(&mask);
269 }
270 let (dx, dy) = (region.x - dst_region.x, region.y - dst_region.y);
272 if layer.blur > 0.0 {
273 gaussian_blur(buf.data_mut(), w, h, layer.blur * k);
274 }
275 let paint = PixmapPaint {
276 opacity: layer.opacity.clamp(0.0, 1.0) as f32,
277 blend_mode: blend_mode(layer.blend),
278 quality: FilterQuality::Nearest,
279 };
280 if let Some(shadow) = &layer.shadow {
281 let mut sh = buf.clone();
283 let [r, g, b, a] = shadow.color.to_rgba8().map(u32::from);
284 for px in sh.data_mut().chunks_exact_mut(4) {
285 let cov = u32::from(px[3]) * a / 255;
286 px[0] = (r * cov / 255) as u8;
287 px[1] = (g * cov / 255) as u8;
288 px[2] = (b * cov / 255) as u8;
289 px[3] = cov as u8;
290 }
291 gaussian_blur(sh.data_mut(), w, h, shadow.blur * k);
292 let off = linear(tf) * shadow.offset.to_point();
293 dst.draw_pixmap(
294 dx,
295 dy,
296 sh.as_ref(),
297 &paint,
298 Transform::from_translate(off.x as f32, off.y as f32),
299 None,
300 );
301 pool.push(sh);
302 }
303 dst.draw_pixmap(dx, dy, buf.as_ref(), &paint, Transform::identity(), None);
304 pool.push(buf);
305}
306
307fn linear(tf: Affine) -> Affine {
309 let [a, b, c, d, _, _] = tf.as_coeffs();
310 Affine::new([a, b, c, d, 0.0, 0.0])
311}
312
313pub(crate) fn blend_mode(b: BlendMode) -> tiny_skia::BlendMode {
314 use tiny_skia::BlendMode as T;
315 match b {
316 BlendMode::Normal => T::SourceOver,
317 BlendMode::Multiply => T::Multiply,
318 BlendMode::Screen => T::Screen,
319 BlendMode::Overlay => T::Overlay,
320 BlendMode::Darken => T::Darken,
321 BlendMode::Lighten => T::Lighten,
322 BlendMode::ColorDodge => T::ColorDodge,
323 BlendMode::ColorBurn => T::ColorBurn,
324 BlendMode::HardLight => T::HardLight,
325 BlendMode::SoftLight => T::SoftLight,
326 BlendMode::Difference => T::Difference,
327 BlendMode::Exclusion => T::Exclusion,
328 BlendMode::Hue => T::Hue,
329 BlendMode::Saturation => T::Saturation,
330 BlendMode::Color => T::Color,
331 BlendMode::Luminosity => T::Luminosity,
332 }
333}
334
335fn draw_prim(dst: &mut Pixmap, prim: &Primitive, tf: Affine, images: &mut BTreeMap<u64, Pixmap>) {
336 match prim {
337 Primitive::Fill { path, paint } => {
338 if let (Some(path), Some(p)) = (to_skia_path(path), skia_paint(paint)) {
339 dst.fill_path(&path, &p, FillRule::Winding, to_skia_transform(tf), None);
340 }
341 }
342 Primitive::Stroke { path, paint, style } => {
343 if let (Some(path), Some(p)) = (to_skia_path(path), skia_paint(paint)) {
344 dst.stroke_path(&path, &p, &skia_stroke(style), to_skia_transform(tf), None);
345 }
346 }
347 Primitive::Image {
348 image,
349 rect,
350 opacity,
351 } => {
352 let src = match images.entry(image.id) {
353 std::collections::btree_map::Entry::Occupied(e) => e.into_mut(),
354 std::collections::btree_map::Entry::Vacant(e) => {
355 let Some(pm) = to_pixmap(image) else { return };
356 e.insert(pm)
357 }
358 };
359 let place = tf
360 * Affine::translate((rect.x0, rect.y0))
361 * Affine::scale_non_uniform(
362 rect.width() / f64::from(image.width),
363 rect.height() / f64::from(image.height),
364 );
365 let paint = PixmapPaint {
366 opacity: *opacity as f32,
367 quality: FilterQuality::Bicubic,
368 ..Default::default()
369 };
370 dst.draw_pixmap(0, 0, src.as_ref(), &paint, to_skia_transform(place), None);
371 }
372 Primitive::PushLayer(_) | Primitive::PopLayer => {}
373 }
374}
375
376pub(crate) fn to_skia_path(path: &BezPath) -> Option<tiny_skia::Path> {
377 let mut pb = PathBuilder::new();
378 for el in path.elements() {
379 match *el {
380 PathEl::MoveTo(p) => pb.move_to(p.x as f32, p.y as f32),
381 PathEl::LineTo(p) => pb.line_to(p.x as f32, p.y as f32),
382 PathEl::QuadTo(a, p) => pb.quad_to(a.x as f32, a.y as f32, p.x as f32, p.y as f32),
383 PathEl::CurveTo(a, b, p) => pb.cubic_to(
384 a.x as f32, a.y as f32, b.x as f32, b.y as f32, p.x as f32, p.y as f32,
385 ),
386 PathEl::ClosePath => pb.close(),
387 }
388 }
389 pb.finish()
390}
391
392fn to_skia_transform(a: Affine) -> Transform {
393 let [a, b, c, d, e, f] = a.as_coeffs().map(|v| v as f32);
394 Transform::from_row(a, b, c, d, e, f)
395}
396
397fn skia_color(c: Color) -> Option<tiny_skia::Color> {
398 tiny_skia::Color::from_rgba(
399 c.r.clamp(0.0, 1.0),
400 c.g.clamp(0.0, 1.0),
401 c.b.clamp(0.0, 1.0),
402 c.a.clamp(0.0, 1.0),
403 )
404}
405
406fn shader(g: &Gradient) -> Option<tiny_skia::Shader<'static>> {
407 let stops: Vec<GradientStop> = g
408 .stops
409 .iter()
410 .filter_map(|(o, c)| Some(GradientStop::new(*o as f32, skia_color(*c)?)))
411 .collect();
412 let pt = |p: kurbo::Point| tiny_skia::Point::from_xy(p.x as f32, p.y as f32);
413 match g.kind {
414 GradientKind::Linear { from, to } => LinearGradient::new(
415 pt(from),
416 pt(to),
417 stops,
418 SpreadMode::Pad,
419 Transform::identity(),
420 ),
421 GradientKind::Radial { center, radius } => RadialGradient::new(
422 pt(center),
423 0.0,
424 pt(center),
425 radius.max(1e-3) as f32,
426 stops,
427 SpreadMode::Pad,
428 Transform::identity(),
429 ),
430 }
431}
432
433fn skia_paint(p: &MPaint) -> Option<Paint<'static>> {
434 let mut paint = Paint::default();
435 match p {
436 MPaint::Solid(c) => paint.set_color(skia_color(*c)?),
437 MPaint::Gradient(g) => paint.shader = shader(g)?,
438 }
439 paint.anti_alias = true;
440 Some(paint)
441}
442
443fn skia_stroke(s: &StrokeStyle) -> Stroke {
444 Stroke {
445 width: s.width as f32,
446 line_cap: match s.cap {
447 LineCap::Butt => tiny_skia::LineCap::Butt,
448 LineCap::Round => tiny_skia::LineCap::Round,
449 LineCap::Square => tiny_skia::LineCap::Square,
450 },
451 line_join: match s.join {
452 LineJoin::Miter => tiny_skia::LineJoin::Miter,
453 LineJoin::Round => tiny_skia::LineJoin::Round,
454 LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
455 },
456 dash: (s.dash.len() >= 2 && s.dash.iter().any(|d| *d > 0.0))
457 .then(|| {
458 let mut d: Vec<f32> = s.dash.iter().map(|v| *v as f32).collect();
460 if d.len() % 2 == 1 {
461 d.extend_from_within(..);
462 }
463 StrokeDash::new(d, s.dash_offset as f32)
464 })
465 .flatten(),
466 ..Default::default()
467 }
468}
469
470fn to_pixmap(img: &RasterImage) -> Option<Pixmap> {
471 let mut pm = Pixmap::new(img.width, img.height)?;
472 for (dst, src) in pm.pixels_mut().iter_mut().zip(img.rgba.chunks_exact(4)) {
473 *dst = tiny_skia::ColorU8::from_rgba(src[0], src[1], src[2], src[3]).premultiply();
474 }
475 Some(pm)
476}
477
478pub(crate) fn demultiply(pixmap: &Pixmap) -> Vec<u8> {
480 let mut data = Vec::with_capacity(pixmap.data().len());
481 for px in pixmap.pixels() {
482 let c = px.demultiply();
483 data.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
484 }
485 data
486}
487
488impl Renderer for CpuRenderer {
489 fn render(&mut self, graph: &SceneGraph, options: &RenderOptions) -> Image {
490 let (w, h) = (options.width.max(1), options.height.max(1));
491 let mut base = match self.pixmap.take() {
492 Some(p) if p.width() == w && p.height() == h => p,
493 _ => Pixmap::new(w, h).expect("valid pixmap size"),
494 };
495 let bg = options.background;
496 base.fill(skia_color(bg).unwrap_or(tiny_skia::Color::BLACK));
497 let view = scene_to_pixels(graph.size(), (w, h));
498 let items = graph.draw_list();
499 self.draw_items(&mut base, &items, view);
500 let data = demultiply(&base);
501 self.pixmap = Some(base);
502 Image {
503 width: w,
504 height: h,
505 data,
506 }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use oxframe_2d::{NodeBuilder, Rect, View};
513
514 use super::*;
515
516 #[test]
517 fn draws_centered_rect() {
518 let graph = SceneGraph::new(100, 100);
519 graph.enter(|| {
520 View::current().add(
521 Rect::new()
522 .size(50.0)
523 .fill(Color::from_hex("#ff0000").unwrap()),
524 );
525 });
526 let img = CpuRenderer::new().render(
527 &graph,
528 &RenderOptions {
529 width: 100,
530 height: 100,
531 background: Color::BLACK,
532 },
533 );
534 let px = |x: usize, y: usize| &img.data[(y * 100 + x) * 4..][..4];
535 assert_eq!(px(50, 50), [255, 0, 0, 255]);
536 assert_eq!(px(10, 10), [0, 0, 0, 255]);
537 assert_eq!(px(74, 50), [255, 0, 0, 255]);
538 assert_eq!(px(76, 50), [0, 0, 0, 255]);
539 }
540
541 #[test]
542 fn a_static_blur_replays_the_same_bytes() {
543 let graph = SceneGraph::new(64, 64);
544 graph.enter(|| {
545 View::current().add(
546 Rect::new()
547 .size(24.0)
548 .fill(Color::from_hex("#ff0000").unwrap())
549 .blur(1.5),
550 );
551 });
552 let opts = RenderOptions {
553 width: 64,
554 height: 64,
555 background: Color::BLACK,
556 };
557 let mut cached = CpuRenderer::new();
558 let first = cached.render(&graph, &opts);
559 let second = cached.render(&graph, &opts);
560 let fresh = CpuRenderer::new().render(&graph, &opts);
561 assert_eq!(first.data, fresh.data);
562 assert_eq!(second.data, fresh.data);
563 }
564
565 #[test]
566 fn a_moved_blur_is_not_served_from_the_old_buffer() {
567 let opts = RenderOptions {
568 width: 64,
569 height: 64,
570 background: Color::BLACK,
571 };
572 let at = |x: f64| {
573 let graph = SceneGraph::new(64, 64);
574 graph.enter(|| {
575 View::current().add(Rect::new().size(16.0).x(x).fill(Color::WHITE).blur(1.0));
576 });
577 graph
578 };
579 let mut renderer = CpuRenderer::new();
580 let before = renderer.render(&at(0.0), &opts);
581 let after = renderer.render(&at(18.0), &opts);
582 assert_ne!(before.data, after.data);
583 }
584}