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            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    // Where this buffer goes in the destination buffer.
272    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        // Shadow = the layer's coverage tinted, blurred and offset.
283        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
308/// The linear part of an affine map (for offsets).
309fn 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                // tiny-skia needs an even count.
464                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
483/// Converts a premultiplied pixmap to straight-alpha RGBA8.
484pub(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    /// Drops decoded images that no item in `used` draws.
495    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}