vello_gpu 0.3.0

A GPU renderer for Vello with CPU-side preprocessing.
Documentation
// Copyright 2026 the Vello Authors and the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Vello GPU glyph rendering backend.
//!
//! Provides [`GlyphAtlas`] and the [`GlyphRenderer`](glifo::GlyphRenderer)
//! implementation for [`Scene`].
//!
//! Unlike the CPU backend, no local `Pixmap` storage is allocated here — the
//! GPU renderer owns atlas textures and receives pixel data through the
//! pending-upload queue.

use core::ops::RangeInclusive;

use crate::{AtlasId, Resources, Scene};
use glifo::atlas::{PendingBitmapUpload, PendingClearRect};
use glifo::renderer::replay_atlas_commands;
use glifo::{
    AtlasCacher, AtlasSlot, DrawSink, GLYPH_PADDING, Glyph, GlyphAtlas, GlyphCacheConfig,
    GlyphRunBackend, ImageCache,
};
use peniko::BlendMode;
use peniko::color::palette::css::BLACK;
use peniko::color::{AlphaColor, Srgb};
use vello_common::kurbo::{Affine, BezPath, Rect};
use vello_common::multi_atlas::AtlasConfig;
use vello_common::paint::{Image, ImageSource, PaintType};
use vello_common::peniko;

/// Glyph atlas cache for the GPU renderer.
#[derive(Debug)]
pub(crate) struct GlyphAtlasResources {
    pub(crate) glyph_atlas: GlyphAtlas,
    pub(crate) glyph_renderer: Scene,
}

impl GlyphAtlasResources {
    pub(crate) fn with_config(
        atlas_width: u16,
        atlas_height: u16,
        eviction_config: GlyphCacheConfig,
    ) -> Self {
        Self {
            glyph_atlas: GlyphAtlas::with_config(eviction_config),
            glyph_renderer: Scene::new(atlas_width, atlas_height),
        }
    }

    pub(crate) fn maintain(&mut self, image_cache: &mut ImageCache) {
        self.glyph_atlas.maintain(image_cache);
    }
}

impl Resources {
    fn ensure_glyph_resources(&mut self) {
        if self.glyph_resources.is_none() {
            let (atlas_width, atlas_height) = self.image_cache.atlas_manager().config().atlas_size;
            // TODO: Use a config optionally provided by the user!
            self.glyph_resources = Some(GlyphAtlasResources::with_config(
                atlas_width,
                atlas_height,
                GlyphCacheConfig::default(),
            ));
        }
    }

    fn atlas_config(&self) -> AtlasConfig {
        *self.image_cache.atlas_manager().config()
    }

    fn atlas_count(&self) -> u32 {
        u32::try_from(self.image_cache.atlas_count()).unwrap()
    }

    fn replay_pending_atlas_commands<E>(
        &mut self,
        mut f: impl FnMut(&Scene, AtlasId) -> Result<(), E>,
    ) -> Result<(), E> {
        if let Some(glyph_resources) = self.glyph_resources.as_mut() {
            glyph_resources
                .glyph_atlas
                .replay_pending_atlas_commands(|recorder| {
                    glyph_resources.glyph_renderer.reset();
                    replay_atlas_commands(
                        &mut recorder.commands,
                        &mut glyph_resources.glyph_renderer,
                    );
                    f(
                        &glyph_resources.glyph_renderer,
                        AtlasId::new(recorder.page_index),
                    )
                })?;
        }

        Ok(())
    }

    pub(crate) fn before_render<T, E>(
        &mut self,
        backend: &mut T,
        mut render_to_atlas: impl FnMut(&mut T, &Scene, u32, AtlasConfig, AtlasId) -> Result<(), E>,
        mut upload_to_atlas: impl FnMut(
            &mut T,
            &ImageCache,
            &PendingBitmapUpload,
            u16,
            u16,
        ) -> Result<(), E>,
    ) -> Result<(), E> {
        let atlas_count = self.atlas_count();
        let atlas_config = self.atlas_config();
        self.replay_pending_atlas_commands(|glyph_renderer, atlas_id| {
            render_to_atlas(backend, glyph_renderer, atlas_count, atlas_config, atlas_id)
        })?;

        const PADDING: u16 = GLYPH_PADDING;

        if let Some(glyph_resources) = self.glyph_resources.as_mut() {
            // TODO: An upload error drops all remaining pending uploads.
            for upload in glyph_resources.glyph_atlas.drain_pending_uploads() {
                let resource = self.image_cache.get(upload.image_id).unwrap();
                let dst_x = resource.offset[0] + PADDING;
                let dst_y = resource.offset[1] + PADDING;
                upload_to_atlas(backend, &self.image_cache, &upload, dst_x, dst_y)?;
            }
        }

        Ok(())
    }

    pub(crate) fn after_render<T, E>(
        &mut self,
        backend: &mut T,
        mut clear_rect: impl FnMut(&mut T, &PendingClearRect) -> Result<(), E>,
    ) -> Result<(), E> {
        self.glyph_prep_cache.maintain();
        if let Some(glyph_resources) = self.glyph_resources.as_mut() {
            glyph_resources.maintain(&mut self.image_cache);
            // TODO: A clear error drops pending clears after their slots were deallocated.
            for rect in glyph_resources.glyph_atlas.drain_pending_clear_rects() {
                clear_rect(backend, &rect)?;
            }
        }

        Ok(())
    }
}

/// [`DrawSink`] for the GPU [`Scene`].
impl DrawSink for Scene {
    #[inline]
    fn set_transform(&mut self, t: Affine) {
        Self::set_transform(self, t);
    }

    #[inline]
    fn set_paint(&mut self, paint: glifo::AtlasPaint) {
        Self::set_paint(self, paint);
    }

    #[inline]
    fn set_paint_transform(&mut self, t: Affine) {
        Self::set_paint_transform(self, t);
    }

    #[inline]
    fn fill_path(&mut self, path: &BezPath) {
        Self::fill_path(self, path);
    }

    #[inline]
    fn fill_rect(&mut self, rect: &Rect) {
        Self::fill_rect(self, rect);
    }

    #[inline]
    fn push_clip_layer(&mut self, clip: &BezPath) {
        Self::push_clip_layer(self, clip);
    }

    #[inline]
    fn push_clip_path(&mut self, clip: &BezPath) {
        Self::push_clip_path(self, clip);
    }

    #[inline]
    fn push_clip_rect(&mut self, clip: &Rect) {
        Self::push_clip_rect(self, clip);
    }

    #[inline]
    fn push_blend_layer(&mut self, blend_mode: BlendMode) {
        Self::push_blend_layer(self, blend_mode);
    }

    #[inline]
    fn pop_layer(&mut self) {
        Self::pop_layer(self);
    }

    #[inline]
    fn pop_clip_path(&mut self) {
        Self::pop_clip(self);
    }

    #[inline]
    fn width(&self) -> u16 {
        Self::width(self)
    }

    #[inline]
    fn height(&self) -> u16 {
        Self::height(self)
    }
}

#[doc(hidden)]
#[derive(Debug)]
pub struct GpuGlyphRunBackend<'a> {
    pub(crate) scene: &'a mut Scene,
    pub(crate) resources: &'a mut Resources,
    pub(crate) atlas_cache_enabled: bool,
}

impl<'a> GpuGlyphRunBackend<'a> {
    fn render_glyphs<Glyphs, R>(
        self,
        run: glifo::GlyphRun<'a>,
        glyphs: Glyphs,
        render: impl FnOnce(&mut glifo::GlyphRunRenderer<'a, 'a, Glyphs>, &mut Scene) -> R,
    ) -> R
    where
        Glyphs: Iterator<Item = Glyph> + Clone,
    {
        let atlas_cacher = if self.atlas_cache_enabled {
            self.resources.ensure_glyph_resources();
            let glyph_resources = self
                .resources
                .glyph_resources
                .as_mut()
                .expect("glyph atlas resources must exist after initialization");
            AtlasCacher::Enabled(
                &mut glyph_resources.glyph_atlas,
                &mut self.resources.image_cache,
            )
        } else {
            AtlasCacher::Disabled
        };

        let mut glyph_run = run.build(
            glyphs,
            self.resources.glyph_prep_cache.as_mut(),
            atlas_cacher,
        );
        render(&mut glyph_run, self.scene)
    }
}

impl<'a> GlyphRunBackend<'a> for GpuGlyphRunBackend<'a> {
    fn atlas_cache(mut self, enabled: bool) -> Self {
        self.atlas_cache_enabled = enabled;
        self
    }

    fn fill_glyphs<Glyphs>(
        self,
        run: glifo::GlyphRun<'a>,
        glyphs: Glyphs,
    ) -> Result<(), glifo::GlyphRenderError>
    where
        Glyphs: Iterator<Item = Glyph> + Clone,
    {
        self.render_glyphs(run, glyphs, |glyph_run, scene| glyph_run.fill_glyphs(scene))
    }

    fn stroke_glyphs<Glyphs>(
        self,
        run: glifo::GlyphRun<'a>,
        glyphs: Glyphs,
    ) -> Result<(), glifo::GlyphRenderError>
    where
        Glyphs: Iterator<Item = Glyph> + Clone,
    {
        self.render_glyphs(run, glyphs, |glyph_run, scene| {
            let stroke_adjustment = glyph_run.stroke_adjustment();
            let original_width = scene.stroke().width;
            scene.stroke_mut().width *= stroke_adjustment;
            let result = glyph_run.stroke_glyphs(scene);
            scene.stroke_mut().width = original_width;
            result
        })
    }

    fn render_decoration<Glyphs>(
        self,
        run: glifo::GlyphRun<'a>,
        glyphs: Glyphs,
        x_range: RangeInclusive<f32>,
        baseline_y: f32,
        offset: f32,
        size: f32,
        buffer: f32,
    ) where
        Glyphs: Iterator<Item = Glyph> + Clone,
    {
        self.render_glyphs(run, glyphs, |glyph_run, scene| {
            glyph_run.render_decoration(x_range, baseline_y, offset, size, buffer, scene);
        });
    }
}

/// A glyph run builder.
pub type GlyphRunBuilder<'a> = glifo::GlyphRunBuilder<'a, GpuGlyphRunBackend<'a>>;

impl glifo::GlyphRenderer for Scene {
    type SavedState = vello_common::render_state::RenderState;

    #[inline]
    fn save_state(&mut self) -> Self::SavedState {
        self.save_current_state()
    }

    #[inline]
    fn restore_state(&mut self, state: Self::SavedState) {
        Self::restore_state(self, state);
    }

    #[inline]
    fn stroke_path(&mut self, path: &BezPath) {
        Self::stroke_path(self, path);
    }

    #[inline]
    fn set_paint_image(&mut self, image: Image) {
        self.set_paint(image);
    }

    #[inline]
    fn set_tint(&mut self, tint: Option<vello_common::paint::Tint>) {
        Self::set_tint(self, tint);
    }

    #[inline]
    fn get_context_color(&self) -> AlphaColor<Srgb> {
        let paint = self.paint().clone();
        match paint {
            PaintType::Solid(s) => s,
            _ => BLACK,
        }
    }

    #[inline]
    fn current_paint(&self) -> &PaintType {
        self.paint()
    }

    #[inline]
    fn atlas_image_source(&self, atlas_slot: &AtlasSlot) -> ImageSource {
        ImageSource::opaque_id(atlas_slot.image_id)
    }

    #[inline]
    fn atlas_paint_transform(&self, _atlas_slot: &AtlasSlot) -> Affine {
        let padding = GLYPH_PADDING as f64;
        Affine::translate((-padding, -padding))
    }
}