vello_cpu 0.0.7

A CPU-based renderer for Vello, optimized for SIMD and multithreaded execution.
Documentation
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Integration of the experimental Vello API preview into Vello CPU.
//!
//! <div class="warning">
//!
//! Vello API is currently in an experimental phase, released only as a preview, and has no stability guarantees.
//! See [its documentation](vello_api) for more details.
//!
//! </div>

use vello_api::{
    PaintScene, Scene,
    exact::ExactPathElements,
    peniko::Style,
    scene::{RenderCommand, extract_integer_translation},
    texture::TextureId,
};
use vello_common::{
    kurbo::{self, Affine, BezPath},
    paint::{ImageId, ImageSource},
    peniko::{BlendMode, Brush, Color, Fill, ImageBrush},
};

use crate::RenderContext;

/// An adapter to implement [`PaintScene`] for Vello CPU's ['`Scene`'][RenderContext] type.
///
/// This type exists to avoid breaking the other APIs in this crate whilst we land/stabilise Vello API.
#[derive(Debug)]
pub struct CPUScenePainter {
    /// The underlying render context. This is public on an interim basis, whilst we decide how
    /// Vello API will develop further.
    pub render_context: RenderContext,
}

impl PaintScene for CPUScenePainter {
    fn append(
        &mut self,
        mut scene_transform: Affine,
        Scene {
            // Make sure we consider all the fields of Scene by destructuring
            paths: input_paths,
            commands: input_commands,

            hinted: input_hinted,
        }: &Scene,
    ) -> Result<(), ()> {
        if *input_hinted {
            if let Some((dx, dy)) = extract_integer_translation(scene_transform) {
                // Update the transform to be a pure integer translation.
                // This is valid as the scene is hinted, so we know it won't be later scaled.
                // As such, a displacement of up to 1/100 of a pixel is inperceptible, but it
                // makes our reasoning about this easier.
                scene_transform = Affine::translate((dx, dy));
            } else {
                // Translation not hinting compatible.
                return Err(());
            }
        }
        for command in input_commands {
            match command {
                RenderCommand::DrawPath(affine, path_id) => {
                    self.render_context.set_transform(scene_transform * *affine);
                    let path = &input_paths.meta[usize::try_from(path_id.0).unwrap()];
                    let path_end = &input_paths
                        .meta
                        .get(usize::try_from(path_id.0).unwrap() + 1)
                        .map_or(input_paths.elements.len(), |it| it.start_index);
                    let segments = &input_paths.elements[path.start_index..*path_end];
                    // Obviously, ideally we'd not be allocating here. This is forced by the current public API of Vello CPU.
                    let bezpath = BezPath::from_iter(segments.iter().cloned());
                    match &path.operation {
                        Style::Stroke(stroke) => {
                            self.render_context.set_stroke(stroke.clone());
                            self.render_context.stroke_path(&bezpath);
                        }
                        Style::Fill(fill) => {
                            self.render_context.set_fill_rule(*fill);
                            self.render_context.fill_path(&bezpath);
                        }
                    }
                }
                RenderCommand::PushLayer(push_layer_command) => {
                    self.render_context
                        .set_transform(push_layer_command.clip_transform);
                    let clip_path = if let Some(path_id) = push_layer_command.clip_path {
                        let path = &input_paths.meta[usize::try_from(path_id.0).unwrap()];
                        // TODO: Also correctly support the case where the meta has a `Style::Stroke`
                        let path_end = &input_paths
                            .meta
                            .get(usize::try_from(path_id.0).unwrap() + 1)
                            .map_or(input_paths.elements.len(), |it| it.start_index);
                        let segments = &input_paths.elements[path.start_index..*path_end];
                        // TODO: Obviously, ideally we'd not be allocating here. This is forced by the current public API of Vello CPU.
                        let bezpath = BezPath::from_iter(segments.iter().cloned());
                        Some(bezpath)
                    } else {
                        None
                    };
                    self.render_context.push_layer(
                        clip_path.as_ref(),
                        push_layer_command.blend_mode,
                        push_layer_command.opacity,
                        None,
                        None,
                    );
                }
                RenderCommand::PopLayer => self.render_context.pop_layer(),
                RenderCommand::SetPaint(paint_transform, brush) => {
                    self.render_context.set_paint_transform(*paint_transform);
                    let brush = match brush {
                        Brush::Solid(alpha_color) => Brush::Solid(*alpha_color),
                        Brush::Gradient(gradient) => Brush::Gradient(gradient.clone()),
                        Brush::Image(brush) => {
                            let image_index =
                                brush.image.to_raw().try_into().expect("Handle this.");
                            Brush::Image(ImageBrush {
                                image: ImageSource::opaque_id(ImageId::new(image_index)),
                                sampler: brush.sampler,
                            })
                        }
                    };
                    self.render_context.set_paint(brush);
                }
                RenderCommand::BlurredRoundedRectPaint(_) => {
                    unimplemented!(
                        "Vello CPU doesn't expose drawing a blurred rounded rectangle in custom shapes yet."
                    )
                }
            }
        }

        Ok(())
    }

    fn fill_path(&mut self, transform: Affine, fill_rule: Fill, path: &impl ExactPathElements) {
        self.render_context.set_transform(transform);
        self.render_context.set_fill_rule(fill_rule);
        // However, using `to_path` avoids allocation in some cases.
        // TODO: Tweak inner API to accept an `ExactPathElements` (or at least, the resultant iterator)
        // That would avoid the superfluous allocation here.
        self.render_context
            .fill_path(&path.exact_path_elements().collect());
    }

    fn stroke_path(
        &mut self,
        transform: Affine,
        stroke_params: &kurbo::Stroke,
        path: &impl ExactPathElements,
    ) {
        self.render_context.set_transform(transform);
        self.render_context.set_stroke(stroke_params.clone());
        // TODO: As in `fill_path`
        self.render_context
            .stroke_path(&path.exact_path_elements().collect());
    }

    fn set_brush(
        &mut self,
        brush: impl Into<Brush<ImageBrush<TextureId>>>,
        paint_transform: Affine,
    ) {
        self.render_context.set_paint_transform(paint_transform);
        let brush = match brush.into() {
            Brush::Solid(alpha_color) => Brush::Solid(alpha_color),
            Brush::Gradient(gradient) => Brush::Gradient(gradient),
            Brush::Image(brush) => {
                // TODO: Make this read more easily.
                let image_index = brush.image.to_raw().try_into().expect("Handle this.");
                Brush::Image(ImageBrush {
                    image: ImageSource::opaque_id(ImageId::new(image_index)),
                    sampler: brush.sampler,
                })
            }
        };
        self.render_context.set_paint(brush);
    }

    fn set_blurred_rounded_rect_brush(
        &mut self,
        _paint_transform: Affine,
        _color: Color,
        _rect: &kurbo::Rect,
        _radius: f32,
        _std_dev: f32,
    ) {
        // This is "trivially" fixable.
        unimplemented!(
            "Vello CPU doesn't expose drawing a blurred rounded rectangle in custom shapes yet."
        )
    }

    fn fill_blurred_rounded_rect(
        &mut self,
        transform: Affine,
        color: Color,
        rect: &kurbo::Rect,
        radius: f32,
        std_dev: f32,
    ) {
        self.render_context.set_paint(color);
        self.render_context.set_transform(transform);
        self.render_context
            .fill_blurred_rounded_rect(rect, radius, std_dev);
    }

    fn push_layer(
        &mut self,
        clip_transform: Affine,
        clip_path: Option<&impl ExactPathElements>,
        blend_mode: Option<BlendMode>,
        opacity: Option<f32>,
        // mask: Option<Mask>,
    ) {
        // We set the fill rule to nonzero for the clip path as a reasonable default.
        // We should make it user provided in the future
        self.render_context.set_fill_rule(Fill::NonZero);
        self.render_context.set_transform(clip_transform);
        self.render_context.push_layer(
            // TODO: As in `fill_path`
            clip_path
                .map(|it| it.exact_path_elements().collect())
                .as_ref(),
            blend_mode,
            opacity,
            None,
            None,
        );
    }

    fn push_clip_layer(&mut self, clip_transform: Affine, path: &impl ExactPathElements) {
        self.render_context.set_fill_rule(Fill::NonZero);
        self.render_context.set_transform(clip_transform);
        // TODO: As in `fill_path`
        self.render_context
            .push_clip_layer(&path.exact_path_elements().collect());
    }

    fn pop_layer(&mut self) {
        self.render_context.pop_layer();
    }
}