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    seen: bool,
35}
36
37impl CpuRenderer {
38    /// A CPU renderer; buffers are reused between frames.
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Renders draw items (with `view` mapping scene → pixels) into a
44    /// transparent pixmap. Used by the GPU renderer for filtered layers.
45    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        // Open layers: buffer, the region it covers, layer, transform, cache key, replayed.
79        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/// A pixel rectangle of the output frame.
167#[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    /// Maps frame pixels to this region's buffer.
177    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
190/// Pixel bounds of everything between the `PushLayer` at `start` and its
191/// `PopLayer`, grown by the reach of blurs and shadows, clipped to `within`.
192fn 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    // Where this buffer goes in the destination buffer.
271    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        // Shadow = the layer's coverage tinted, blurred and offset.
282        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
307/// The linear part of an affine map (for offsets).
308fn 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                // tiny-skia needs an even count.
459                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
478/// Converts a premultiplied pixmap to straight-alpha RGBA8.
479pub(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}