Skip to main content

oxframe_render/
cpu.rs

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/// tiny-skia backend. Deterministic: same scene, same bytes.
18#[derive(Default)]
19pub struct CpuRenderer {
20    pixmap: Option<Pixmap>,
21    images: BTreeMap<u64, Pixmap>,
22    /// Reused layer buffers.
23    pool: Vec<Pixmap>,
24    /// Pre-composite pixels of layers whose vectors did not change.
25    layers: Vec<CachedLayer>,
26    /// Items left to ignore because their layer was replayed from [`Self::layers`].
27    skip: u32,
28}
29
30struct CachedLayer {
31    key: u64,
32    region: Region,
33    buf: Pixmap,
34    /// The blurred shadow made from `buf`, with the key it was made for.
35    shadow: Option<(u64, Pixmap)>,
36    seen: bool,
37}
38
39impl CpuRenderer {
40    /// A CPU renderer; buffers are reused between frames.
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Renders draw items (with `view` mapping scene → pixels) into a
46    /// transparent pixmap. Used by the GPU renderer for filtered layers.
47    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        // Open layers: buffer, the region it covers, layer, transform, cache key, replayed.
81        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        // A shadow depends on the content (key), the clip, blur and shadow
163        // paint, and the scale: while those hold, blurring it again is waste.
164        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/// A pixel rectangle of the output frame.
196#[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    /// Maps frame pixels to this region's buffer.
206    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
219/// Pixel bounds of everything between the `PushLayer` at `start` and its
220/// `PopLayer`, grown by the reach of blurs and shadows, clipped to `within`.
221fn 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/// Draws a finished layer into `dst`. Its shadow is `cached` when given, else
278/// made here and returned, so the caller can keep it for the next frame.
279#[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    // Where this buffer goes in the destination buffer.
305    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            // Shadow = the layer's coverage tinted, blurred and offset.
318            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
346/// The linear part of an affine map (for offsets).
347fn 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                // tiny-skia needs an even count.
502                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
521/// Converts a premultiplied pixmap to straight-alpha RGBA8.
522pub(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    /// Drops decoded images that no item in `used` draws.
533    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}