Added a Vello back-end with a custom winit viewer.

My first PR! 🎉

You can get Rust with [Rustup](https://rustup.rs). Then run `cargo run --release` in the `vello/` directory.

Things left to do before merging:
- [x] remove `clap`
- [x] add rustfmt & clippy checks to CI
- [x] add text support
- [x] add README
- [ ] add CONTRIBUTING

Diffs=
abe5aab14 Added a Vello back-end with a custom winit viewer. (#5786)

Co-authored-by: DragoČ™ Tiselice <dragos@rive.app>
Co-authored-by: DragoČ™ Tiselice <dragostiselice@gmail.com>
diff --git a/.rive_head b/.rive_head
index 0d382b8..498b00a 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-adeebb26a7ac58a1aaa18a40ed9d98af46735c97
+abe5aab1431c34a42458a9650831d6b3a7c39feb
diff --git a/vello/Cargo.toml b/vello/Cargo.toml
new file mode 100644
index 0000000..4a4d533
--- /dev/null
+++ b/vello/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "rive-vello"
+version = "0.1.0"
+edition = "2021"
+
+[build-dependencies]
+cc = { version = "1.0", features = ["parallel"] }
+walkdir = "2.3.3"
+
+[dependencies]
+clap = { version = "4.3.19", features = ["derive"] }
+image = "0.24.6"
+pollster = "0.3.0"
+smallvec = "1.8.0"
+vello = { git = "https://github.com/linebender/vello", rev = "3cb5462" }
+wgpu = "0.17.0"
+winit = "0.28.6"
diff --git a/vello/README.md b/vello/README.md
new file mode 100644
index 0000000..a59755f
--- /dev/null
+++ b/vello/README.md
@@ -0,0 +1,31 @@
+# Vello back-end for Rive
+
+Small application for viewing `.riv` files rendered with Vello. It uses [winit]
+for creating the window and [image] to decode images.
+
+## Build
+
+You will need clang, Cargo, and the Rust compiler to build. You can can install
+Cargo and rustc using [rustup].
+
+To use the application, run:
+
+```bash
+$ cargo run --release
+```
+
+## Usage
+
+Drop any `.riv` file into the window to open it. Scroll to control the size of
+the grid of copies.
+
+## Caveats
+
+The current implementation is a work-in-progress and might exhibit artifacts or
+render incorrectly.
+
+Only tested on macOS for the time being.
+
+[winit]: https://github.com/rust-windowing/winit
+[image]: https://github.com/image-rs/image
+[rustup]: https://rustup.rs
diff --git a/vello/build.rs b/vello/build.rs
new file mode 100644
index 0000000..c2105ca
--- /dev/null
+++ b/vello/build.rs
@@ -0,0 +1,118 @@
+use std::{
+    env,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+use walkdir::WalkDir;
+
+const CHECKOUT_DIRECTORY: &str = "target";
+
+struct Checkout {
+    path: PathBuf,
+}
+
+impl Checkout {
+    pub fn new(repo: &str, tag: &str) -> Self {
+        let name = repo.rsplit_once('/').expect("URL format invalid").1;
+        let mut path = PathBuf::from(CHECKOUT_DIRECTORY);
+
+        path.push(name);
+
+        if !path.is_dir() {
+            Command::new("git")
+                .args([
+                    "clone",
+                    "-b",
+                    tag,
+                    repo,
+                    &path.as_os_str().to_string_lossy(),
+                ])
+                .output()
+                .unwrap_or_else(|_| panic!("failed to clone {}; is git CLI available?", name));
+        }
+
+        Self { path }
+    }
+
+    pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
+        self.path.join(path)
+    }
+}
+
+fn all_files_with_extension<P: AsRef<Path>>(
+    path: P,
+    extension: &str,
+) -> impl Iterator<Item = PathBuf> + '_ {
+    WalkDir::new(path).into_iter().filter_map(move |entry| {
+        entry
+            .ok()
+            .map(|entry| entry.into_path())
+            .filter(|path| path.extension() == Some(&OsString::from(extension)))
+    })
+}
+
+fn main() {
+    println!("cargo:rerun-if-changed=src/vello_renderer.hpp");
+    println!("cargo:rerun-if-changed=src/vello_renderer.cpp");
+    println!("cargo:rerun-if-changed=src/winit_viewer.cpp");
+
+    let harfbuzz = Checkout::new("https://github.com/harfbuzz/harfbuzz", "8.1.1");
+    let sheen_bidi = Checkout::new("https://github.com/Tehreer/SheenBidi", "v2.6");
+
+    let target = env::var("TARGET").unwrap();
+    let profile = env::var("PROFILE").unwrap();
+
+    let mut cfg = cc::Build::new();
+    cfg.compiler("clang")
+        .cpp(true)
+        .flag_if_supported("-std=c++11") // for unix
+        .warnings(false)
+        .file(harfbuzz.join("src/harfbuzz.cc"));
+
+    if !target.contains("windows") {
+        cfg.define("HAVE_PTHREAD", "1");
+    }
+
+    if target.contains("apple") && profile.contains("release") {
+        cfg.define("HAVE_CORETEXT", "1");
+    }
+
+    if target.contains("windows") {
+        cfg.define("HAVE_DIRECTWRITE", "1");
+    }
+
+    if target.contains("windows-gnu") {
+        cfg.flag("-Wa,-mbig-obj");
+    }
+
+    cfg.compile("harfbuzz");
+
+    cc::Build::new()
+        .compiler("clang")
+        .files(all_files_with_extension(sheen_bidi.join("Source"), "c"))
+        .include(sheen_bidi.join("Headers"))
+        .compile("sheenbidi");
+
+    cc::Build::new()
+        .compiler("clang")
+        .cpp(true)
+        .files(all_files_with_extension("../src", "cpp"))
+        .files(all_files_with_extension(
+            "../viewer/src/viewer_content",
+            "cpp",
+        ))
+        .file("src/vello_renderer.cpp")
+        .file("src/winit_viewer.cpp")
+        .include("src")
+        .include("../include")
+        .include("../viewer/include")
+        .include(harfbuzz.join("src"))
+        .include(sheen_bidi.join("Headers"))
+        .flag("-std=c++14")
+        .flag("-Wno-everything")
+        .define("RIVE_SKIP_IMGUI", None)
+        .define("WITH_RIVE_TEXT", None)
+        .compile("rive");
+}
diff --git a/vello/src/lib.rs b/vello/src/lib.rs
new file mode 100644
index 0000000..f50f50a
--- /dev/null
+++ b/vello/src/lib.rs
@@ -0,0 +1,564 @@
+#![allow(clippy::missing_safety_doc)]
+
+use core::slice;
+use std::{io::Cursor, ptr::NonNull};
+
+use image::io::Reader;
+use smallvec::SmallVec;
+use util::{ScaleFromOrigin, UnwrapAndDeref};
+use vello::{
+    kurbo::{Affine, BezPath, Line, PathSeg, Point, Rect, Shape, Vec2},
+    peniko::{
+        BlendMode, Brush, BrushRef, Cap, Color, ColorStop, ColorStopsSource, Fill, Format,
+        Gradient, Image, Join, Mix, Stroke,
+    },
+    SceneBuilder, SceneFragment,
+};
+
+mod rive;
+mod util;
+mod viewer;
+
+pub use viewer::ViewerContent;
+
+fn from_bgra8(color: u32) -> Color {
+    Color::rgba8(
+        (color >> 16) as u8,
+        (color >> 8) as u8,
+        color as u8,
+        (color >> 24) as u8,
+    )
+}
+
+#[derive(Debug)]
+enum RenderStyle {
+    Fill,
+    Stroke(Stroke),
+}
+
+#[derive(Debug)]
+struct SliceStops<'s> {
+    colors: &'s [u32],
+    stops: &'s [f32],
+}
+
+impl ColorStopsSource for SliceStops<'_> {
+    fn collect_stops(&self, vec: &mut SmallVec<[ColorStop; 4]>) {
+        vec.extend(
+            self.colors
+                .iter()
+                .zip(self.stops.iter())
+                .map(|(&color, &offset)| ColorStop {
+                    offset,
+                    color: from_bgra8(color),
+                }),
+        );
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_gradient_new_linear(
+    sx: f32,
+    sy: f32,
+    ex: f32,
+    ey: f32,
+    colors_data: *const u32,
+    stops_data: *const f32,
+    len: usize,
+) -> Option<NonNull<Gradient>> {
+    let colors = slice::from_raw_parts(colors_data, len);
+    let stops = slice::from_raw_parts(stops_data, len);
+
+    let raw_stops = SliceStops { colors, stops };
+    let gradient =
+        Gradient::new_linear((sx as f64, sy as f64), (ex as f64, ey as f64)).with_stops(raw_stops);
+
+    NonNull::new(Box::into_raw(Box::new(gradient)))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_gradient_new_radial(
+    cx: f32,
+    cy: f32,
+    radius: f32,
+    colors_data: *const u32,
+    stops_data: *const f32,
+    len: usize,
+) -> Option<NonNull<Gradient>> {
+    let colors = slice::from_raw_parts(colors_data, len);
+    let stops = slice::from_raw_parts(stops_data, len);
+
+    let raw_stops = SliceStops { colors, stops };
+    let gradient = Gradient::new_radial((cx as f64, cy as f64), radius).with_stops(raw_stops);
+
+    NonNull::new(Box::into_raw(Box::new(gradient)))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_gradient_release(gradient: Option<NonNull<Gradient>>) {
+    gradient.map(|ptr| Box::from_raw(ptr.as_ptr())).unwrap();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_image_new(data: *const u8, len: usize) -> Option<NonNull<Image>> {
+    let image = Reader::new(Cursor::new(slice::from_raw_parts(data, len)))
+        .with_guessed_format()
+        .ok()?
+        .decode()
+        .ok()?
+        .into_rgba8();
+    let width = image.width();
+    let height = image.height();
+
+    NonNull::new(Box::into_raw(Box::new(Image::new(
+        image.into_raw().into(),
+        Format::Rgba8,
+        width,
+        height,
+    ))))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_image_release(image: Option<NonNull<Image>>) {
+    image.map(|ptr| Box::from_raw(ptr.as_ptr())).unwrap();
+}
+
+#[derive(Debug)]
+pub struct VelloPaint {
+    style: RenderStyle,
+    brush: Brush,
+    blend_mode: BlendMode,
+}
+
+impl Default for VelloPaint {
+    fn default() -> Self {
+        Self {
+            style: RenderStyle::Fill,
+            brush: Brush::Solid(Color::TRANSPARENT),
+            blend_mode: Mix::Normal.into(),
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_new() -> Option<NonNull<VelloPaint>> {
+    NonNull::new(Box::into_raw(Box::default()))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_release(paint: Option<NonNull<VelloPaint>>) {
+    paint.map(|ptr| Box::from_raw(ptr.as_ptr())).unwrap();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_set_style(
+    mut paint: Option<NonNull<VelloPaint>>,
+    style: rive::RenderPaint,
+) {
+    paint.unwrap_and_deref().style = style.into();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_set_color(mut paint: Option<NonNull<VelloPaint>>, color: u32) {
+    paint.unwrap_and_deref().brush = Brush::Solid(from_bgra8(color));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_set_gradient(
+    mut paint: Option<NonNull<VelloPaint>>,
+    mut gradient: Option<NonNull<Gradient>>,
+) {
+    paint.unwrap_and_deref().brush = Brush::Gradient(gradient.unwrap_and_deref().clone());
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_set_thickness(
+    mut paint: Option<NonNull<VelloPaint>>,
+    thickness: f32,
+) {
+    let style = &mut paint.unwrap_and_deref().style;
+    loop {
+        if let RenderStyle::Stroke(stroke) = style {
+            stroke.width = thickness;
+            break;
+        } else {
+            *style = RenderStyle::Stroke(Stroke::new(0.0));
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_set_join(
+    mut paint: Option<NonNull<VelloPaint>>,
+    join: rive::StrokeJoin,
+) {
+    let style = &mut paint.unwrap_and_deref().style;
+    loop {
+        if let RenderStyle::Stroke(stroke) = style {
+            stroke.join = match join {
+                rive::StrokeJoin::Miter => Join::Miter,
+                rive::StrokeJoin::Round => Join::Round,
+                rive::StrokeJoin::Bevel => Join::Bevel,
+            };
+            break;
+        } else {
+            *style = RenderStyle::Stroke(Stroke::new(0.0));
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_set_cap(
+    mut paint: Option<NonNull<VelloPaint>>,
+    cap: rive::StrokeCap,
+) {
+    let style = &mut paint.unwrap_and_deref().style;
+    loop {
+        if let RenderStyle::Stroke(stroke) = style {
+            stroke.start_cap = match cap {
+                rive::StrokeCap::Butt => Cap::Butt,
+                rive::StrokeCap::Round => Cap::Round,
+                rive::StrokeCap::Square => Cap::Square,
+            };
+            stroke.end_cap = stroke.start_cap;
+            break;
+        } else {
+            *style = RenderStyle::Stroke(Stroke::new(0.0));
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_paint_set_blend_mode(
+    mut paint: Option<NonNull<VelloPaint>>,
+    blend_mode: rive::BlendMode,
+) {
+    let mix: Mix = blend_mode.into();
+    paint.unwrap_and_deref().blend_mode = mix.into();
+}
+
+#[derive(Debug)]
+pub struct VelloPath {
+    path: BezPath,
+    fill: Fill,
+}
+
+impl Default for VelloPath {
+    fn default() -> Self {
+        Self {
+            path: Default::default(),
+            fill: Fill::NonZero,
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_new() -> Option<NonNull<VelloPath>> {
+    NonNull::new(Box::into_raw(Box::default()))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_release(path: Option<NonNull<VelloPath>>) {
+    path.map(|ptr| Box::from_raw(ptr.as_ptr())).unwrap();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_set_fill_rule(
+    mut path: Option<NonNull<VelloPath>>,
+    fill_rule: rive::FillRule,
+) {
+    path.unwrap_and_deref().fill = match fill_rule {
+        rive::FillRule::NonZero => Fill::NonZero,
+        rive::FillRule::EvenOdd => Fill::EvenOdd,
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_rewind(mut path: Option<NonNull<VelloPath>>) {
+    path.unwrap_and_deref().path.truncate(0);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_extend(
+    mut path: Option<NonNull<VelloPath>>,
+    mut from: Option<NonNull<VelloPath>>,
+    mut transform: Option<NonNull<[f32; 6]>>,
+) {
+    let mut from = from.unwrap_and_deref().path.clone();
+    from.apply_affine(Affine::new(transform.unwrap_and_deref().map(Into::into)));
+
+    path.unwrap_and_deref()
+        .path
+        .extend(from.elements().iter().cloned());
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_move_to(mut path: Option<NonNull<VelloPath>>, x: f32, y: f32) {
+    path.unwrap_and_deref()
+        .path
+        .move_to(Point::new(x as f64, y as f64));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_line_to(mut path: Option<NonNull<VelloPath>>, x: f32, y: f32) {
+    path.unwrap_and_deref()
+        .path
+        .line_to(Point::new(x as f64, y as f64));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_cubic_to(
+    mut path: Option<NonNull<VelloPath>>,
+    ox: f32,
+    oy: f32,
+    ix: f32,
+    iy: f32,
+    x: f32,
+    y: f32,
+) {
+    path.unwrap_and_deref().path.curve_to(
+        Point::new(ox as f64, oy as f64),
+        Point::new(ix as f64, iy as f64),
+        Point::new(x as f64, y as f64),
+    );
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_path_close(mut path: Option<NonNull<VelloPath>>) {
+    path.unwrap_and_deref().path.close_path();
+}
+
+pub struct VelloRenderer {
+    pub scene: Box<SceneFragment>,
+    builder: SceneBuilder<'static>,
+    transforms: Vec<Affine>,
+    clips: Vec<bool>,
+}
+
+impl VelloRenderer {
+    fn last_transform(&mut self) -> &mut Affine {
+        self.transforms.last_mut().unwrap()
+    }
+
+    fn last_clip(&mut self) -> &mut bool {
+        self.clips.last_mut().unwrap()
+    }
+}
+
+impl Default for VelloRenderer {
+    fn default() -> Self {
+        let mut scene = Box::<SceneFragment>::default();
+        let builder = {
+            let scene_mut: &mut SceneFragment = &mut scene;
+            SceneBuilder::for_fragment(unsafe {
+                // Quite a hack until we have a better way to do this in Vello.
+                // Pretend that the scene fragment pointer lives for 'static.
+                std::mem::transmute(scene_mut)
+            })
+        };
+
+        Self {
+            scene,
+            builder,
+            transforms: vec![Affine::IDENTITY],
+            clips: vec![false],
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_renderer_save(mut renderer: Option<NonNull<VelloRenderer>>) {
+    let renderer = renderer.unwrap_and_deref();
+
+    let last_transform = *renderer.last_transform();
+
+    renderer.transforms.push(last_transform);
+    renderer.clips.push(false);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_renderer_restore(mut renderer: Option<NonNull<VelloRenderer>>) {
+    let renderer = renderer.unwrap_and_deref();
+
+    renderer.transforms.pop();
+    if renderer.clips.pop().unwrap_or_default() {
+        renderer.builder.pop_layer();
+    }
+
+    if renderer.transforms.is_empty() {
+        renderer.transforms.push(Affine::IDENTITY);
+        renderer.clips.push(false);
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_renderer_transform(
+    mut renderer: Option<NonNull<VelloRenderer>>,
+    transform: *const [f32; 6],
+) {
+    let last_transform = renderer.unwrap_and_deref().last_transform();
+    *last_transform *= Affine::new((*transform).map(Into::into));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_renderer_draw_path(
+    mut renderer: Option<NonNull<VelloRenderer>>,
+    mut path: Option<NonNull<VelloPath>>,
+    mut paint: Option<NonNull<VelloPaint>>,
+) {
+    let renderer = renderer.unwrap_and_deref();
+    let path = path.unwrap_and_deref();
+    let paint = paint.unwrap_and_deref();
+
+    let transform = *renderer.last_transform();
+
+    let builder = &mut renderer.builder;
+
+    let skip_blending = paint.blend_mode == Mix::Normal.into();
+
+    if !skip_blending {
+        builder.push_layer(paint.blend_mode, 1.0, transform, &path.path.bounding_box());
+    }
+
+    match &paint.style {
+        RenderStyle::Fill => builder.fill(path.fill, transform, &paint.brush, None, &path.path),
+        RenderStyle::Stroke(stroke) => {
+            builder.stroke(stroke, transform, &paint.brush, None, &path.path)
+        }
+    }
+
+    if !skip_blending {
+        builder.pop_layer();
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_renderer_clip_path(
+    mut renderer: Option<NonNull<VelloRenderer>>,
+    mut clip: Option<NonNull<VelloPath>>,
+) {
+    let renderer = renderer.unwrap_and_deref();
+
+    let transform = *renderer.last_transform();
+
+    if *renderer.last_clip() {
+        renderer.builder.pop_layer();
+    }
+
+    renderer
+        .builder
+        .push_layer(Mix::Clip, 1.0, transform, &clip.unwrap_and_deref().path);
+
+    *renderer.last_clip() = true;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_renderer_draw_image(
+    mut renderer: Option<NonNull<VelloRenderer>>,
+    mut image: Option<NonNull<Image>>,
+    blend_mode: rive::BlendMode,
+    opacity: f32,
+) {
+    let renderer = renderer.unwrap_and_deref();
+    let image = image.unwrap_and_deref();
+    let mix: Mix = blend_mode.into();
+
+    let transform = renderer.last_transform().pre_translate(Vec2::new(
+        image.width as f64 * -0.5,
+        image.height as f64 * -0.5,
+    ));
+    let rect = Rect::new(0.0, 0.0, image.width as f64, image.height as f64);
+
+    let builder = &mut renderer.builder;
+
+    let skip_blending = mix == Mix::Normal && opacity == 1.0;
+
+    if skip_blending {
+        builder.push_layer(mix, opacity, transform, &rect);
+    }
+
+    builder.draw_image(image, transform);
+
+    if skip_blending {
+        builder.pop_layer();
+    }
+}
+
+fn triangle_path(points: [Point; 3]) -> BezPath {
+    BezPath::from_path_segments(
+        [
+            PathSeg::Line(Line::new(points[0], points[1])),
+            PathSeg::Line(Line::new(points[1], points[2])),
+            PathSeg::Line(Line::new(points[2], points[0])),
+        ]
+        .into_iter(),
+    )
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn vello_renderer_draw_image_mesh(
+    mut renderer: Option<NonNull<VelloRenderer>>,
+    mut image: Option<NonNull<Image>>,
+    vertices_data: *const rive::Vec2D,
+    vertices_len: usize,
+    uvs_data: *const rive::Vec2D,
+    uvs_len: usize,
+    indices_data: *const u16,
+    indices_len: usize,
+    blend_mode: rive::BlendMode,
+    opacity: f32,
+) {
+    let renderer = renderer.unwrap_and_deref();
+    let image = image.unwrap_and_deref();
+
+    let vertices = slice::from_raw_parts(vertices_data, vertices_len);
+    let uvs = slice::from_raw_parts(uvs_data, uvs_len);
+    let indices = slice::from_raw_parts(indices_data, indices_len);
+
+    let mix: Mix = blend_mode.into();
+
+    for triangle_indices in indices.chunks_exact(3) {
+        let points = [
+            vertices[triangle_indices[0] as usize],
+            vertices[triangle_indices[1] as usize],
+            vertices[triangle_indices[2] as usize],
+        ];
+        let uvs = [
+            uvs[triangle_indices[0] as usize],
+            uvs[triangle_indices[1] as usize],
+            uvs[triangle_indices[2] as usize],
+        ];
+
+        let center = Point::new(
+            ((points[0].x + points[1].x + points[2].x) / 3.0) as f64,
+            ((points[0].y + points[1].y + points[2].y) / 3.0) as f64,
+        );
+
+        let path = triangle_path(points.map(|v| Point::new(v.x as f64, v.y as f64)));
+
+        let transform = renderer
+            .last_transform()
+            .pre_scale_from_origin(1.03, center);
+        let brush_transform = util::map_uvs_to_triangle(&points, &uvs, image.width, image.height);
+
+        let builder = &mut renderer.builder;
+
+        let skip_blending = mix == Mix::Normal;
+
+        if !skip_blending {
+            builder.push_layer(mix, opacity, transform, &path.bounding_box());
+        }
+
+        builder.fill(
+            Fill::NonZero,
+            transform,
+            BrushRef::Image(image),
+            Some(brush_transform),
+            &path,
+        );
+
+        if !skip_blending {
+            builder.pop_layer();
+        }
+    }
+}
diff --git a/vello/src/main.rs b/vello/src/main.rs
new file mode 100644
index 0000000..fadb30f
--- /dev/null
+++ b/vello/src/main.rs
@@ -0,0 +1,229 @@
+use std::time::Instant;
+
+use rive_vello::{VelloRenderer, ViewerContent};
+use vello::{
+    kurbo::{Affine, Rect, Vec2},
+    peniko::{Color, Fill},
+    util::{RenderContext, RenderSurface},
+    Renderer, RendererOptions, Scene, SceneBuilder,
+};
+use winit::{
+    dpi::LogicalSize,
+    event::{ElementState, Event, MouseButton, WindowEvent},
+    event_loop::{ControlFlow, EventLoop},
+    window::{Window, WindowBuilder},
+};
+
+struct RenderState {
+    surface: RenderSurface,
+    window: Window,
+}
+
+const INITIAL_WINDOW_SIZE: LogicalSize<u32> = LogicalSize::new(700, 700);
+const FRAME_STATS_CAPACITY: usize = 30;
+const SCROLL_FACTOR_THRESHOLD: f64 = 100.0;
+
+fn main() {
+    let mut viewer_content: Option<ViewerContent> = None;
+
+    let event_loop = EventLoop::new();
+    let mut cached_window: Option<Window> = None;
+    let mut renderer: Option<Renderer> = None;
+    let mut render_cx = RenderContext::new().unwrap();
+    let mut render_state: Option<RenderState> = None;
+
+    let mut mouse_pos = Vec2::default();
+    let mut scroll_delta = 0.0;
+    let mut frame_start_time = Instant::now();
+    let mut stats = Vec::with_capacity(FRAME_STATS_CAPACITY);
+
+    event_loop.run(move |event, _event_loop, control_flow| match event {
+        Event::WindowEvent { ref event, .. } => {
+            let Some(render_state) = &mut render_state else { return };
+
+            match event {
+                WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
+                WindowEvent::Resized(size) => {
+                    if let Some(viewer_content) = &viewer_content {
+                        viewer_content.handle_resize(size.width, size.height);
+                    }
+
+                    render_cx.resize_surface(&mut render_state.surface, size.width, size.height);
+                    render_state.window.request_redraw();
+                }
+                WindowEvent::MouseInput {
+                    state,
+                    button: MouseButton::Left,
+                    ..
+                } => {
+                    if let Some(viewer_content) = &viewer_content {
+                        let handler = match state {
+                            ElementState::Pressed => ViewerContent::handle_pointer_down,
+                            ElementState::Released => ViewerContent::handle_pointer_up,
+                        };
+
+                        handler(viewer_content, mouse_pos);
+                    }
+                }
+                WindowEvent::CursorMoved { position, .. } => {
+                    mouse_pos = Vec2::new(position.x, position.y);
+                    if let Some(viewer_content) = &viewer_content {
+                        viewer_content.handle_pointer_move(mouse_pos);
+                    }
+                }
+                WindowEvent::MouseWheel { delta, .. } => match delta {
+                    winit::event::MouseScrollDelta::LineDelta(_, lines_y) => {
+                        scroll_delta = (scroll_delta
+                            - (*lines_y as f64).signum() * SCROLL_FACTOR_THRESHOLD)
+                            .max(0.0);
+                    }
+                    winit::event::MouseScrollDelta::PixelDelta(pixels) => {
+                        scroll_delta = (scroll_delta - pixels.y).max(0.0);
+                    }
+                },
+                WindowEvent::DroppedFile(path) => {
+                    viewer_content = ViewerContent::new(path);
+
+                    if let Some(viewer_content) = &viewer_content {
+                        let size = render_state.window.inner_size();
+                        viewer_content.handle_resize(size.width, size.height);
+                    }
+                }
+                _ => {}
+            }
+        }
+        Event::MainEventsCleared => {
+            if let Some(render_state) = &mut render_state {
+                render_state.window.request_redraw();
+            }
+        }
+        Event::RedrawRequested(_) => {
+            let mut vello_renderer = VelloRenderer::default();
+            let factor = (scroll_delta / SCROLL_FACTOR_THRESHOLD).max(1.0) as u32;
+
+            let elapsed = &frame_start_time.elapsed();
+            stats.push(elapsed.as_secs_f64());
+
+            if stats.len() == FRAME_STATS_CAPACITY {
+                let average = stats.drain(..).sum::<f64>() / FRAME_STATS_CAPACITY as f64;
+
+                if let Some(state) = &mut render_state {
+                    let copies = (factor > 1)
+                        .then(|| format!(" ({} copies)", factor.pow(2)))
+                        .unwrap_or_default();
+                    state.window.set_title(&format!(
+                        "Rive on Vello demo | {:.2}ms{}",
+                        average * 1000.0,
+                        copies
+                    ));
+                }
+            }
+
+            frame_start_time = Instant::now();
+
+            let Some(render_state) = &mut render_state else { return };
+            let width = render_state.surface.config.width;
+            let height = render_state.surface.config.height;
+            let device_handle = &render_cx.devices[render_state.surface.dev_id];
+
+            let render_params = vello::RenderParams {
+                base_color: Color::DIM_GRAY,
+                width,
+                height,
+            };
+
+            let surface_texture = render_state
+                .surface
+                .surface
+                .get_current_texture()
+                .expect("failed to get surface texture");
+
+            let mut scene = Scene::default();
+            let mut builder = SceneBuilder::for_scene(&mut scene);
+
+            if let Some(viewer_content) = &mut viewer_content {
+                viewer_content.handle_draw(&mut vello_renderer, elapsed.as_secs_f64());
+
+                for i in 0..factor.pow(2) {
+                    builder.append(
+                        &vello_renderer.scene,
+                        Some(
+                            Affine::default()
+                                .then_scale(1.0 / factor as f64)
+                                .then_translate(Vec2::new(
+                                    (i % factor) as f64 * width as f64 / factor as f64,
+                                    (i / factor) as f64 * height as f64 / factor as f64,
+                                )),
+                        ),
+                    );
+                }
+            } else {
+                // Vello currently crashes when rendering an empty scene.
+                builder.fill(
+                    Fill::NonZero,
+                    Affine::IDENTITY,
+                    Color::TRANSPARENT,
+                    None,
+                    &Rect::new(0.0, 0.0, 0.0, 0.0),
+                );
+            }
+
+            vello::block_on_wgpu(
+                &device_handle.device,
+                renderer.as_mut().unwrap().render_to_surface_async(
+                    &device_handle.device,
+                    &device_handle.queue,
+                    &scene,
+                    &surface_texture,
+                    &render_params,
+                ),
+            )
+            .expect("failed to render to surface");
+
+            surface_texture.present();
+            device_handle.device.poll(wgpu::Maintain::Poll);
+        }
+        Event::Suspended => {
+            if let Some(render_state) = render_state.take() {
+                cached_window = Some(render_state.window);
+            }
+            *control_flow = ControlFlow::Wait;
+        }
+        Event::Resumed => {
+            if render_state.is_some() {
+                return;
+            }
+
+            let window = cached_window.take().unwrap_or_else(|| {
+                WindowBuilder::new()
+                    .with_inner_size(INITIAL_WINDOW_SIZE)
+                    .with_resizable(true)
+                    .with_title("Rive on Vello demo")
+                    .build(_event_loop)
+                    .unwrap()
+            });
+            let size = window.inner_size();
+            let surface_future = render_cx.create_surface(&window, size.width, size.height);
+
+            let surface = pollster::block_on(surface_future).expect("Error creating surface");
+            render_state = {
+                let render_state = RenderState { window, surface };
+                renderer = Some(
+                    Renderer::new(
+                        &render_cx.devices[render_state.surface.dev_id].device,
+                        &RendererOptions {
+                            surface_format: Some(render_state.surface.format),
+                            timestamp_period: render_cx.devices[render_state.surface.dev_id]
+                                .queue
+                                .get_timestamp_period(),
+                        },
+                    )
+                    .expect("Could create renderer"),
+                );
+                Some(render_state)
+            };
+            *control_flow = ControlFlow::Poll;
+        }
+        _ => {}
+    });
+}
diff --git a/vello/src/rive.rs b/vello/src/rive.rs
new file mode 100644
index 0000000..7c6c222
--- /dev/null
+++ b/vello/src/rive.rs
@@ -0,0 +1,98 @@
+use vello::peniko::{Mix, Stroke};
+
+use crate::RenderStyle;
+
+#[repr(C)]
+#[derive(Debug, Default)]
+pub enum RenderPaint {
+    Stroke,
+    #[default]
+    Fill,
+}
+
+impl From<RenderPaint> for RenderStyle {
+    fn from(value: RenderPaint) -> Self {
+        match value {
+            RenderPaint::Stroke => Self::Stroke(Stroke::new(0.0)),
+            RenderPaint::Fill => Self::Fill,
+        }
+    }
+}
+
+#[repr(u32)]
+#[derive(Debug, Default)]
+pub enum StrokeJoin {
+    #[default]
+    Miter = 0,
+    Round = 1,
+    Bevel = 2,
+}
+
+#[repr(u32)]
+#[derive(Debug, Default)]
+pub enum StrokeCap {
+    #[default]
+    Butt = 0,
+    Round = 1,
+    Square = 2,
+}
+
+#[repr(u32)]
+#[derive(Debug, Default)]
+pub enum BlendMode {
+    #[default]
+    SrcOver = 3,
+    Screen = 14,
+    Overlay = 15,
+    Darken = 16,
+    Lighten = 17,
+    ColorDodge = 18,
+    ColorBurn = 19,
+    HardLight = 20,
+    SoftLight = 21,
+    Difference = 22,
+    Exclusion = 23,
+    Multiply = 24,
+    Hue = 25,
+    Saturation = 26,
+    Color = 27,
+    Luminosity = 28,
+}
+
+impl From<BlendMode> for Mix {
+    fn from(value: BlendMode) -> Self {
+        match value {
+            BlendMode::SrcOver => Self::Normal,
+            BlendMode::Screen => Self::Screen,
+            BlendMode::Overlay => Self::Overlay,
+            BlendMode::Darken => Self::Darken,
+            BlendMode::Lighten => Self::Lighten,
+            BlendMode::ColorDodge => Self::ColorDodge,
+            BlendMode::ColorBurn => Self::ColorBurn,
+            BlendMode::HardLight => Self::HardLight,
+            BlendMode::SoftLight => Self::SoftLight,
+            BlendMode::Difference => Self::Difference,
+            BlendMode::Exclusion => Self::Exclusion,
+            BlendMode::Multiply => Self::Multiply,
+            BlendMode::Hue => Self::Hue,
+            BlendMode::Saturation => Self::Saturation,
+            BlendMode::Color => Self::Color,
+            BlendMode::Luminosity => Self::Luminosity,
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Debug, Default)]
+pub enum FillRule {
+    #[default]
+    NonZero,
+    EvenOdd,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct Vec2D {
+    pub x: f32,
+    pub y: f32,
+}
diff --git a/vello/src/util.rs b/vello/src/util.rs
new file mode 100644
index 0000000..aba56d9
--- /dev/null
+++ b/vello/src/util.rs
@@ -0,0 +1,157 @@
+use std::{
+    ops::{Add, Mul, Neg, Sub},
+    ptr::NonNull,
+};
+
+use vello::kurbo::{Affine, Point};
+
+use crate::rive;
+
+pub trait UnwrapAndDeref {
+    type Deref;
+
+    unsafe fn unwrap_and_deref(&mut self) -> &mut Self::Deref;
+}
+
+impl<T> UnwrapAndDeref for Option<NonNull<T>> {
+    type Deref = T;
+
+    unsafe fn unwrap_and_deref(&mut self) -> &mut Self::Deref {
+        self.map(|mut ptr| ptr.as_mut()).unwrap()
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+struct Vec2 {
+    x: f32,
+    y: f32,
+}
+
+impl Vec2 {
+    pub fn new(x: f32, y: f32) -> Self {
+        Self { x, y }
+    }
+
+    pub fn splat(val: f32) -> Self {
+        Self::new(val, val)
+    }
+}
+
+impl Add for Vec2 {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self::new(self.x + rhs.x, self.y + rhs.y)
+    }
+}
+
+impl Sub for Vec2 {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self::new(self.x - rhs.x, self.y - rhs.y)
+    }
+}
+
+impl Mul for Vec2 {
+    type Output = Self;
+
+    fn mul(self, rhs: Self) -> Self::Output {
+        Self::new(self.x * rhs.x, self.y * rhs.y)
+    }
+}
+
+impl Mul<f32> for Vec2 {
+    type Output = Self;
+
+    fn mul(self, rhs: f32) -> Self::Output {
+        self * Self::splat(rhs)
+    }
+}
+
+impl Neg for Vec2 {
+    type Output = Self;
+
+    fn neg(self) -> Self::Output {
+        Self::new(-self.x, -self.y)
+    }
+}
+
+/// Finds the affine transform that maps triangle `from` to triangle `to`. The algorithm is based
+/// on the [Simplex Affine Mapping] method which has a [Swift implementation].
+///
+/// [Simplex Affine Mapping]: https://www.researchgate.net/publication/332410209_Beginner%27s_guide_to_mapping_simplexes_affinely
+/// [Swift implementation]: https://rethunk.medium.com/finding-an-affine-transform-using-three-2d-point-correspondences-using-simplex-affine-mapping-255aeb4e8055
+fn simplex_affine_mapping(from: [Vec2; 3], to: [Vec2; 3]) -> Affine {
+    let [a, b, c] = from;
+    let [d, e, f] = to;
+
+    let det_recip = (a.x * b.y + b.x * c.y + c.x * a.y - a.x * c.y - b.x * a.y - c.x * b.y).recip();
+
+    let p = (d * (b.y - c.y) - e * (a.y - c.y) + f * (a.y - b.y)) * det_recip;
+
+    let q = (e * (a.x - c.x) - d * (b.x - c.x) - f * (a.x - b.x)) * det_recip;
+
+    let t = (d * (b.x * c.y - b.y * c.x) - e * (a.x * c.y - a.y * c.x)
+        + f * (a.x * b.y - a.y * b.x))
+        * det_recip;
+
+    Affine::new([
+        p.x as f64, p.y as f64, q.x as f64, q.y as f64, t.x as f64, t.y as f64,
+    ])
+}
+
+pub fn map_uvs_to_triangle(
+    points: &[rive::Vec2D; 3],
+    uvs: &[rive::Vec2D; 3],
+    width: u32,
+    height: u32,
+) -> Affine {
+    simplex_affine_mapping(
+        uvs.map(|v| Vec2::new(v.x * width as f32, v.y * height as f32)),
+        points.map(|v| Vec2::new(v.x, v.y)),
+    )
+}
+
+pub trait ScaleFromOrigin {
+    fn pre_scale_from_origin(self, scale: f64, origin: Point) -> Self;
+}
+
+impl ScaleFromOrigin for Affine {
+    fn pre_scale_from_origin(self, scale: f64, origin: Point) -> Self {
+        let origin = origin.to_vec2();
+        self.pre_translate(origin)
+            .pre_scale(scale)
+            .pre_translate(-origin)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use vello::kurbo;
+
+    #[test]
+    fn straightforward_sam() {
+        let from = [
+            Vec2::new(1.0, 1.0),
+            Vec2::new(3.0, 1.0),
+            Vec2::new(1.0, 3.0),
+        ];
+        let to = [
+            Vec2::new(4.0, 4.0),
+            Vec2::new(4.0, -1.0),
+            Vec2::new(-2.0, 4.0),
+        ];
+
+        let mapping = simplex_affine_mapping(from, to);
+
+        for (from, to) in from.into_iter().zip(to.into_iter()) {
+            let res = mapping * kurbo::Point::new(from.x as f64, from.y as f64);
+
+            assert_eq!(res.x as f32, to.x);
+            assert_eq!(res.y as f32, to.y);
+        }
+    }
+}
diff --git a/vello/src/vello_renderer.cpp b/vello/src/vello_renderer.cpp
new file mode 100644
index 0000000..63c94e7
--- /dev/null
+++ b/vello/src/vello_renderer.cpp
@@ -0,0 +1,181 @@
+#include "vello_renderer.hpp"
+
+VelloPath::~VelloPath() { vello_path_release(m_path); }
+
+void VelloPath::rewind() { vello_path_rewind(m_path); }
+
+void VelloPath::addRenderPath(RenderPath* path, const Mat2D& transform)
+{
+    vello_path_extend(m_path, static_cast<VelloPath*>(path)->m_path, transform.values());
+}
+
+void VelloPath::fillRule(FillRule value) { vello_path_set_fill_rule(m_path, value); }
+
+void VelloPath::moveTo(float x, float y) { vello_path_move_to(m_path, x, y); }
+
+void VelloPath::lineTo(float x, float y) { vello_path_line_to(m_path, x, y); }
+
+void VelloPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y)
+{
+    vello_path_cubic_to(m_path, ox, oy, ix, iy, x, y);
+}
+
+void VelloPath::close() { vello_path_close(m_path); }
+
+VelloGradient::~VelloGradient() { vello_gradient_release(m_gradient); }
+
+VelloImage::~VelloImage() { vello_image_release(m_image); }
+
+VelloPaint::~VelloPaint() { vello_paint_release(m_paint); }
+
+void VelloPaint::style(RenderPaintStyle style) { vello_paint_set_style(m_paint, style); }
+
+void VelloPaint::color(unsigned int value) { vello_paint_set_color(m_paint, value); }
+
+void VelloPaint::thickness(float value) { vello_paint_set_thickness(m_paint, value); }
+
+void VelloPaint::join(StrokeJoin value) { vello_paint_set_join(m_paint, value); }
+
+void VelloPaint::cap(StrokeCap value) { vello_paint_set_cap(m_paint, value); }
+
+void VelloPaint::blendMode(BlendMode value) { vello_paint_set_blend_mode(m_paint, value); }
+
+void VelloPaint::shader(rcp<RenderShader> shader)
+{
+    VelloGradient* gradient = (VelloGradient*)shader.get();
+
+    if (gradient != nullptr)
+    {
+        vello_paint_set_gradient(m_paint, gradient->gradient());
+    }
+}
+
+void VelloRenderer::save() { vello_renderer_save(m_renderer); }
+
+void VelloRenderer::restore() { vello_renderer_restore(m_renderer); }
+
+void VelloRenderer::transform(const Mat2D& transform)
+{
+    vello_renderer_transform(m_renderer, transform.values());
+}
+
+void VelloRenderer::clipPath(RenderPath* path)
+{
+    vello_renderer_clip_path(m_renderer, static_cast<VelloPath*>(path)->path());
+}
+
+void VelloRenderer::drawPath(RenderPath* path, RenderPaint* paint)
+{
+    vello_renderer_draw_path(m_renderer,
+                             static_cast<VelloPath*>(path)->path(),
+                             static_cast<VelloPaint*>(paint)->paint());
+}
+
+void VelloRenderer::drawImage(const RenderImage* image, BlendMode blend_mode, float opacity)
+{
+    vello_renderer_draw_image(m_renderer,
+                              static_cast<const VelloImage*>(image)->image(),
+                              blend_mode,
+                              opacity);
+}
+
+void VelloRenderer::drawImageMesh(const RenderImage* image,
+                                  rcp<RenderBuffer> vertices,
+                                  rcp<RenderBuffer> uvCoords,
+                                  rcp<RenderBuffer> indices,
+                                  BlendMode blendMode,
+                                  float opacity)
+{
+    vello_renderer_draw_image_mesh(m_renderer,
+                                   static_cast<const VelloImage*>(image)->image(),
+                                   DataRenderBuffer::Cast(vertices.get())->vecs(),
+                                   vertices->count(),
+                                   DataRenderBuffer::Cast(uvCoords.get())->vecs(),
+                                   uvCoords->count(),
+                                   DataRenderBuffer::Cast(indices.get())->u16s(),
+                                   indices->count(),
+                                   blendMode,
+                                   opacity);
+}
+
+rcp<RenderBuffer> VelloFactory::makeBufferU16(Span<const uint16_t> data)
+{
+    return DataRenderBuffer::Make(data);
+}
+
+rcp<RenderBuffer> VelloFactory::makeBufferU32(Span<const uint32_t> data)
+{
+    return DataRenderBuffer::Make(data);
+}
+
+rcp<RenderBuffer> VelloFactory::makeBufferF32(Span<const float> data)
+{
+    return DataRenderBuffer::Make(data);
+}
+
+rcp<RenderShader> VelloFactory::makeLinearGradient(float sx,
+                                                   float sy,
+                                                   float ex,
+                                                   float ey,
+                                                   const ColorInt colors[], // [count]
+                                                   const float stops[],     // [count]
+                                                   size_t count)
+{
+    RawVelloGradient gradient = vello_gradient_new_linear(sx, sy, ex, ey, colors, stops, count);
+    return rcp<RenderShader>(new VelloGradient(std::move(gradient)));
+}
+
+rcp<RenderShader> VelloFactory::makeRadialGradient(float cx,
+                                                   float cy,
+                                                   float radius,
+                                                   const ColorInt colors[], // [count]
+                                                   const float stops[],     // [count]
+                                                   size_t count)
+{
+    RawVelloGradient gradient = vello_gradient_new_radial(cx, cy, radius, colors, stops, count);
+    return rcp<RenderShader>(new VelloGradient(std::move(gradient)));
+}
+
+std::unique_ptr<RenderPath> VelloFactory::makeRenderPath(RawPath& rawPath, FillRule fillRule)
+{
+    std::unique_ptr<VelloPath> path = std::make_unique<VelloPath>();
+
+    for (auto [verb, points] : rawPath)
+    {
+        switch (verb)
+        {
+            case PathVerb::move:
+                path->move(points[0]);
+                break;
+            case PathVerb::line:
+                path->line(points[1]);
+                break;
+            case PathVerb::cubic:
+                path->cubic(points[1], points[2], points[3]);
+                break;
+            case PathVerb::close:
+                path->close();
+                break;
+        }
+    }
+
+    return path;
+}
+
+std::unique_ptr<RenderPath> VelloFactory::makeEmptyRenderPath()
+{
+    return std::make_unique<VelloPath>();
+}
+
+std::unique_ptr<RenderPaint> VelloFactory::makeRenderPaint()
+{
+    return std::make_unique<VelloPaint>();
+}
+
+std::unique_ptr<RenderImage> VelloFactory::decodeImage(Span<const uint8_t> encoded)
+{
+    return std::make_unique<VelloImage>(vello_image_new(encoded.data(), encoded.size()));
+}
+
+static VelloFactory factory;
+rive::Factory* ViewerContent::RiveFactory() { return &factory; }
diff --git a/vello/src/vello_renderer.hpp b/vello/src/vello_renderer.hpp
new file mode 100644
index 0000000..5ef84d2
--- /dev/null
+++ b/vello/src/vello_renderer.hpp
@@ -0,0 +1,222 @@
+#ifndef _RIVE_VELLO_RENDERER_HPP_
+#define _RIVE_VELLO_RENDERER_HPP_
+
+#include "rive/factory.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/path_types.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/renderer.hpp"
+#include "utils/factory_utils.hpp"
+#include "viewer/viewer_content.hpp"
+
+#include "vello_renderer.hpp"
+
+using namespace rive;
+
+typedef void* RawVelloPath;
+typedef void* RawVelloGradient;
+typedef void* RawVelloImage;
+typedef void* RawVelloPaint;
+typedef void* RawVelloRenderer;
+
+extern "C"
+{
+    typedef void* RawVelloPath;
+    typedef void* RawVelloGradient;
+    typedef void* RawVelloImage;
+    typedef void* RawVelloPaint;
+    typedef void* RawVelloRenderer;
+
+    const RawVelloPath vello_path_new();
+    void vello_path_release(const RawVelloPath path);
+    void vello_path_rewind(const RawVelloPath path);
+    void vello_path_extend(const RawVelloPath path, void* other, const float* transform);
+    void vello_path_set_fill_rule(const RawVelloPath path, FillRule fill_rule);
+    void vello_path_move_to(const RawVelloPath path, float x, float y);
+    void vello_path_line_to(const RawVelloPath path, float x, float y);
+    void vello_path_cubic_to(const RawVelloPath path,
+                             float ox,
+                             float oy,
+                             float ix,
+                             float iy,
+                             float x,
+                             float y);
+    void vello_path_close(const RawVelloPath path);
+
+    const RawVelloGradient vello_gradient_new_linear(float sx,
+                                                     float sy,
+                                                     float ex,
+                                                     float ey,
+                                                     const uint32_t* colors_data,
+                                                     const float* stops_data,
+                                                     size_t len);
+    const RawVelloGradient vello_gradient_new_radial(float cx,
+                                                     float cy,
+                                                     float radius,
+                                                     const uint32_t* colors_data,
+                                                     const float* stops_data,
+                                                     size_t len);
+    void vello_gradient_release(const RawVelloGradient gradient);
+
+    const RawVelloImage vello_image_new(const uint8_t* data, size_t len);
+    void vello_image_release(const RawVelloImage image);
+
+    const RawVelloPaint vello_paint_new();
+    void vello_paint_release(const RawVelloPaint paint);
+    void vello_paint_set_style(const RawVelloPaint paint, RenderPaintStyle style);
+    void vello_paint_set_color(const RawVelloPaint paint, uint32_t color);
+    void vello_paint_set_thickness(const RawVelloPaint paint, float thickness);
+    void vello_paint_set_join(const RawVelloPaint paint, StrokeJoin join);
+    void vello_paint_set_cap(const RawVelloPaint paint, StrokeCap cap);
+    void vello_paint_set_blend_mode(const RawVelloPaint paint, BlendMode blend_mode);
+    void vello_paint_set_gradient(const RawVelloPaint paint, const RawVelloGradient gradient);
+
+    const RawVelloRenderer vello_renderer_new();
+    void vello_renderer_release(const RawVelloRenderer renderer);
+    void vello_renderer_save(const RawVelloRenderer renderer);
+    void vello_renderer_restore(const RawVelloRenderer renderer);
+    void vello_renderer_transform(const RawVelloRenderer renderer, const float* transform);
+    void vello_renderer_clip_path(const RawVelloRenderer renderer, const RawVelloPath path);
+    void vello_renderer_draw_path(const RawVelloRenderer renderer,
+                                  const RawVelloPath path,
+                                  const RawVelloPaint paint);
+    void vello_renderer_draw_image(const RawVelloRenderer renderer,
+                                   const RawVelloImage image,
+                                   BlendMode blend_mode,
+                                   float opacity);
+    void vello_renderer_draw_image_mesh(const RawVelloRenderer renderer,
+                                        const RawVelloImage image,
+                                        const Vec2D* vertices_data,
+                                        size_t vertices_len,
+                                        const Vec2D* uvs_data,
+                                        size_t uvs_len,
+                                        const uint16_t* indices_data,
+                                        size_t indices_len,
+                                        BlendMode blend_mode,
+                                        float opacity);
+}
+
+using namespace rive;
+
+class VelloPath : public RenderPath
+{
+private:
+    RawVelloPath m_path;
+
+public:
+    VelloPath() : m_path(vello_path_new()) {}
+    VelloPath(RawVelloPath& path) : m_path(std::move(path)) {}
+    ~VelloPath() override;
+
+    const RawVelloPath& path() const { return m_path; }
+
+    void rewind() override;
+    void addRenderPath(RenderPath* path, const Mat2D& transform) override;
+    void fillRule(FillRule value) override;
+    void moveTo(float x, float y) override;
+    void lineTo(float x, float y) override;
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
+    virtual void close() override;
+};
+
+class VelloGradient : public RenderShader
+{
+private:
+    RawVelloGradient m_gradient;
+
+public:
+    VelloGradient(RawVelloGradient gradient) : m_gradient(gradient) {}
+    ~VelloGradient() override;
+
+    const RawVelloGradient& gradient() const { return m_gradient; }
+};
+
+class VelloImage : public RenderImage
+{
+private:
+    RawVelloImage m_image;
+
+public:
+    VelloImage(RawVelloImage image) : m_image(image) {}
+    ~VelloImage() override;
+
+    const RawVelloImage& image() const { return m_image; }
+};
+
+class VelloPaint : public RenderPaint
+{
+private:
+    RawVelloPaint m_paint;
+
+public:
+    VelloPaint() : m_paint(vello_paint_new()) {}
+    ~VelloPaint() override;
+
+    const RawVelloPaint& paint() const { return m_paint; }
+
+    void style(RenderPaintStyle style) override;
+    void color(unsigned int value) override;
+    void thickness(float value) override;
+    void join(StrokeJoin value) override;
+    void cap(StrokeCap value) override;
+    void blendMode(BlendMode value) override;
+    void shader(rcp<RenderShader>) override;
+    void invalidateStroke() override {}
+};
+
+class VelloRenderer : public Renderer
+{
+protected:
+    RawVelloRenderer m_renderer;
+
+public:
+    VelloRenderer() : m_renderer(vello_renderer_new()) {}
+    VelloRenderer(RawVelloRenderer renderer) : m_renderer(std::move(renderer)) {}
+    ~VelloRenderer() override {}
+
+    void save() override;
+    void restore() override;
+    void transform(const Mat2D& transform) override;
+    void clipPath(RenderPath* path) override;
+    void drawPath(RenderPath* path, RenderPaint* paint) override;
+    void drawImage(const RenderImage*, BlendMode, float opacity) override;
+    void drawImageMesh(const RenderImage*,
+                       rcp<RenderBuffer> vertices_f32,
+                       rcp<RenderBuffer> uvCoords_f32,
+                       rcp<RenderBuffer> indices_u16,
+                       BlendMode,
+                       float opacity) override;
+};
+
+class VelloFactory : public Factory
+{
+public:
+    rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
+    rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
+    rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
+
+    rcp<RenderShader> makeLinearGradient(float sx,
+                                         float sy,
+                                         float ex,
+                                         float ey,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderShader> makeRadialGradient(float cx,
+                                         float cy,
+                                         float radius,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    std::unique_ptr<RenderPath> makeRenderPath(RawPath&, FillRule) override;
+
+    std::unique_ptr<RenderPath> makeEmptyRenderPath() override;
+
+    std::unique_ptr<RenderPaint> makeRenderPaint() override;
+
+    std::unique_ptr<RenderImage> decodeImage(Span<const uint8_t>) override;
+};
+
+#endif
diff --git a/vello/src/viewer.rs b/vello/src/viewer.rs
new file mode 100644
index 0000000..2b51a58
--- /dev/null
+++ b/vello/src/viewer.rs
@@ -0,0 +1,113 @@
+use std::{
+    ffi::CString, fs::File, io::Read, os::unix::prelude::OsStrExt, path::Path, ptr::NonNull,
+};
+
+use vello::kurbo::Vec2;
+
+use crate::VelloRenderer;
+
+#[derive(Debug)]
+enum RawViewerContent {}
+
+extern "C" {
+    fn viewer_content_new(raw_path: *const i8) -> Option<NonNull<RawViewerContent>>;
+    fn viewer_content_release(raw_viewer_content: Option<NonNull<RawViewerContent>>);
+    fn viewer_content_handle_resize(
+        raw_viewer_content: Option<NonNull<RawViewerContent>>,
+        width: i32,
+        height: i32,
+    );
+    // We're simply propagating the `VelloRender` pointer opaquely through the FFI.
+    #[allow(improper_ctypes)]
+    fn viewer_content_handle_draw(
+        raw_viewer_content: Option<NonNull<RawViewerContent>>,
+        raw_vello_renderer: Option<NonNull<VelloRenderer>>,
+        elapsed: f64,
+    );
+    fn viewer_content_handle_pointer_move(
+        raw_viewer_content: Option<NonNull<RawViewerContent>>,
+        x: f32,
+        y: f32,
+    );
+    fn viewer_content_handle_pointer_down(
+        raw_viewer_content: Option<NonNull<RawViewerContent>>,
+        x: f32,
+        y: f32,
+    );
+    fn viewer_content_handle_pointer_up(
+        raw_viewer_content: Option<NonNull<RawViewerContent>>,
+        x: f32,
+        y: f32,
+    );
+}
+
+#[derive(Debug)]
+pub struct ViewerContent {
+    raw_viewer_content: Option<NonNull<RawViewerContent>>,
+}
+
+impl ViewerContent {
+    pub fn new<P: AsRef<Path>>(path: P) -> Option<Self> {
+        let path = path.as_ref();
+
+        let header = {
+            let mut file = File::open(path).ok()?;
+            let mut buffer = [0; 4];
+
+            file.read_exact(&mut buffer).ok()?;
+
+            buffer
+        };
+
+        if &header != b"RIVE" {
+            return None;
+        }
+
+        let c_str = CString::new(path.as_os_str().as_bytes()).unwrap();
+        let raw_viewer_content = Some(unsafe { viewer_content_new(c_str.as_ptr())? });
+
+        Some(Self { raw_viewer_content })
+    }
+
+    pub fn handle_resize(&self, width: u32, height: u32) {
+        unsafe {
+            viewer_content_handle_resize(self.raw_viewer_content, width as i32, height as i32);
+        }
+    }
+
+    pub fn handle_draw(&mut self, renderer: &mut VelloRenderer, elapsed: f64) {
+        unsafe {
+            viewer_content_handle_draw(
+                self.raw_viewer_content,
+                NonNull::new(renderer as *mut VelloRenderer),
+                elapsed,
+            )
+        }
+    }
+
+    pub fn handle_pointer_move(&self, pos: Vec2) {
+        unsafe {
+            viewer_content_handle_pointer_move(self.raw_viewer_content, pos.x as f32, pos.y as f32);
+        }
+    }
+
+    pub fn handle_pointer_down(&self, pos: Vec2) {
+        unsafe {
+            viewer_content_handle_pointer_down(self.raw_viewer_content, pos.x as f32, pos.y as f32);
+        }
+    }
+
+    pub fn handle_pointer_up(&self, pos: Vec2) {
+        unsafe {
+            viewer_content_handle_pointer_up(self.raw_viewer_content, pos.x as f32, pos.y as f32);
+        }
+    }
+}
+
+impl Drop for ViewerContent {
+    fn drop(&mut self) {
+        unsafe {
+            viewer_content_release(self.raw_viewer_content);
+        }
+    }
+}
diff --git a/vello/src/winit_viewer.cpp b/vello/src/winit_viewer.cpp
new file mode 100644
index 0000000..9406af1
--- /dev/null
+++ b/vello/src/winit_viewer.cpp
@@ -0,0 +1,48 @@
+#include "viewer/viewer_content.hpp"
+
+#include "vello_renderer.hpp"
+
+extern "C"
+{
+    typedef void* RawViewerContent;
+
+    const RawViewerContent viewer_content_new(const char* filename)
+    {
+        return static_cast<void*>(ViewerContent::findHandler(filename).release());
+    }
+
+    void viewer_content_release(const RawViewerContent viewer_content)
+    {
+        std::unique_ptr<ViewerContent> val(std::move(static_cast<ViewerContent*>(viewer_content)));
+    }
+
+    void viewer_content_handle_resize(const RawViewerContent viewer_content,
+                                      int32_t width,
+                                      int32_t height)
+    {
+        static_cast<ViewerContent*>(viewer_content)->handleResize(width, height);
+    }
+
+    void viewer_content_handle_draw(const RawViewerContent viewer_content,
+                                    RawVelloRenderer raw_renderer,
+                                    double elapsed)
+    {
+        VelloRenderer renderer = VelloRenderer(raw_renderer);
+        static_cast<ViewerContent*>(viewer_content)->handleDraw(&renderer, elapsed);
+    }
+
+    void viewer_content_handle_pointer_move(const RawViewerContent viewer_content, float x, float y)
+    {
+        static_cast<ViewerContent*>(viewer_content)->handlePointerMove(x, y);
+    }
+
+    void viewer_content_handle_pointer_down(const RawViewerContent viewer_content, float x, float y)
+    {
+        static_cast<ViewerContent*>(viewer_content)->handlePointerDown(x, y);
+    }
+
+    void viewer_content_handle_pointer_up(const RawViewerContent viewer_content, float x, float y)
+    {
+        static_cast<ViewerContent*>(viewer_content)->handlePointerUp(x, y);
+    }
+}
diff --git a/viewer/include/viewer/viewer_content.hpp b/viewer/include/viewer/viewer_content.hpp
index ad7ba60..2dde428 100644
--- a/viewer/include/viewer/viewer_content.hpp
+++ b/viewer/include/viewer/viewer_content.hpp
@@ -5,10 +5,14 @@
 #ifndef _RIVE_VIEWER_CONTENT_HPP_
 #define _RIVE_VIEWER_CONTENT_HPP_
 
+#include <vector>
+
 #include "rive/span.hpp"
 #include "rive/refcnt.hpp"
 
+#ifndef RIVE_SKIP_IMGUI
 #include "imgui.h"
+#endif
 
 namespace rive
 {
@@ -24,7 +28,9 @@
 
     virtual void handleResize(int width, int height) = 0;
     virtual void handleDraw(rive::Renderer* renderer, double elapsed) = 0;
+#ifndef RIVE_SKIP_IMGUI
     virtual void handleImgui() = 0;
+#endif
 
     virtual void handlePointerMove(float x, float y) {}
     virtual void handlePointerDown(float x, float y) {}
diff --git a/viewer/src/viewer_content/image_content.cpp b/viewer/src/viewer_content/image_content.cpp
index ab24415..6bdb84d 100644
--- a/viewer/src/viewer_content/image_content.cpp
+++ b/viewer/src/viewer_content/image_content.cpp
@@ -19,7 +19,9 @@
     }
 
     void handleResize(int width, int height) override {}
+#ifndef RIVE_SKIP_IMGUI
     void handleImgui() override {}
+#endif
 };
 
 std::unique_ptr<ViewerContent> ViewerContent::Image(const char filename[])
diff --git a/viewer/src/viewer_content/scene_content.cpp b/viewer/src/viewer_content/scene_content.cpp
index 196000f..5b94ea3 100644
--- a/viewer/src/viewer_content/scene_content.cpp
+++ b/viewer/src/viewer_content/scene_content.cpp
@@ -209,6 +209,7 @@
         renderer->restore();
     }
 
+#ifndef RIVE_SKIP_IMGUI
     void handleImgui() override
     {
         // For now the atlas packer only works with tess as it compiles in our
@@ -379,6 +380,7 @@
             ImGui::Text("Drop a .riv file to preview.");
         }
     }
+#endif
 };
 
 std::unique_ptr<ViewerContent> ViewerContent::Scene(const char filename[])
@@ -389,4 +391,4 @@
         return rivestd::make_unique<SceneContent>(filename, std::move(file));
     }
     return nullptr;
-}
\ No newline at end of file
+}
diff --git a/viewer/src/viewer_content/text_content.cpp b/viewer/src/viewer_content/text_content.cpp
index 81f2637..d455b78 100644
--- a/viewer/src/viewer_content/text_content.cpp
+++ b/viewer/src/viewer_content/text_content.cpp
@@ -338,6 +338,7 @@
     }
 
     void handleResize(int width, int height) override {}
+#ifndef RIVE_SKIP_IMGUI
     void handleImgui() override
     {
         const char* alignOptions[] = {"left", "right", "center"};
@@ -347,6 +348,7 @@
         ImGui::Combo("combo", &m_align, alignOptions, IM_ARRAYSIZE(alignOptions));
         ImGui::End();
     }
+#endif
 };
 
 static bool ends_width(const char str[], const char suffix[])
diff --git a/viewer/src/viewer_content/textpath_content.cpp b/viewer/src/viewer_content/textpath_content.cpp
index 7c9071b..351da53 100644
--- a/viewer/src/viewer_content/textpath_content.cpp
+++ b/viewer/src/viewer_content/textpath_content.cpp
@@ -387,6 +387,7 @@
 
     void handleResize(int width, int height) override {}
 
+#ifndef RIVE_SKIP_IMGUI
     void handleImgui() override
     {
         ImGui::Begin("path", nullptr);
@@ -398,6 +399,7 @@
         ImGui::SliderFloat("Flare radius", &m_flareRadius, 10, 100);
         ImGui::End();
     }
+#endif
 };
 
 std::unique_ptr<ViewerContent> ViewerContent::TextPath(const char filename[])
diff --git a/viewer/src/viewer_content/trimpath_content.cpp b/viewer/src/viewer_content/trimpath_content.cpp
index 2f4dac2..d63a00b 100644
--- a/viewer/src/viewer_content/trimpath_content.cpp
+++ b/viewer/src/viewer_content/trimpath_content.cpp
@@ -159,6 +159,7 @@
 
     void handleResize(int width, int height) override {}
 
+#ifndef RIVE_SKIP_IMGUI
     void handleImgui() override
     {
         ImGui::Begin("trim", nullptr);
@@ -166,6 +167,7 @@
         ImGui::SliderFloat("To", &m_trimTo, 0, 1);
         ImGui::End();
     }
+#endif
 };
 
 std::unique_ptr<ViewerContent> ViewerContent::TrimPath(const char[])
diff --git a/viewer/src/viewer_content/viewer_content.cpp b/viewer/src/viewer_content/viewer_content.cpp
index 44a63b3..a8ba142 100644
--- a/viewer/src/viewer_content/viewer_content.cpp
+++ b/viewer/src/viewer_content/viewer_content.cpp
@@ -63,9 +63,11 @@
     printf("\n");
 }
 
+#ifndef RIVE_SKIP_IMGUI
 #include "viewer/viewer_host.hpp"
 
 rive::Factory* ViewerContent::RiveFactory() { return ViewerHost::Factory(); }
+#endif
 
 #include "rive/text/font_hb.hpp"
 rive::rcp<rive::Font> ViewerContent::DecodeFont(rive::Span<const uint8_t> span)