Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/anyrender_vello_cpu/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this crate are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Opt-in support for Vello's (experimental) glyph atlas cache, enabled at runtime via
`VelloCpuScenePainter::set_glyph_caching` / `VelloCpuImageRenderer::set_glyph_caching`.

## [0.18.0] - 2026-10-03

### Changed
Expand Down
9 changes: 9 additions & 0 deletions crates/anyrender_vello_cpu/src/image_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ impl VelloCpuImageRenderer {
}
}

/// Enable or disable caching of rasterized glyphs in Vello's glyph atlas.
///
/// Defaults to `false`.
///
/// Note: Vello considers atlas-backed glyph caching experimental.
pub fn set_glyph_caching(&mut self, enabled: bool) {
self.scene.set_glyph_caching(enabled);
}

/// Drop all cached image conversions.
pub fn clear_image_cache(&mut self) {
self.scene.clear_image_cache();
Expand Down
25 changes: 25 additions & 0 deletions crates/anyrender_vello_cpu/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ use crate::image_cache::{ImageCache, ImageCacheConfig};

const DEFAULT_TOLERANCE: f64 = 0.1;

/// Whether a glyph run drawn with the given transforms can use Vello's glyph atlas cache.
///
/// Vello only supports caching glyphs that are drawn upright with a uniform scale. It checks
/// this itself when inserting a glyph into the atlas, but not when looking one up, so a skewed
/// (e.g. synthetic italic) or rotated run would otherwise be drawn using upright cached glyphs.
fn supports_glyph_caching(transform: Affine, glyph_transform: Option<Affine>) -> bool {
let [a, b, c, d, _, _] = (transform * glyph_transform.unwrap_or_default()).as_coeffs();
b == 0.0 && c == 0.0 && a == d && a > 0.0
}

enum LayerKind {
Layer,
Clip,
Expand All @@ -20,6 +30,7 @@ pub struct VelloCpuScenePainter {
pub(crate) resources: vello_cpu::Resources,
pub(crate) image_cache: ImageCache,
layer_stack: Vec<LayerKind>,
glyph_caching: bool,
}

impl VelloCpuScenePainter {
Expand All @@ -33,9 +44,19 @@ impl VelloCpuScenePainter {
resources: vello_cpu::Resources::new(),
image_cache: ImageCache::new(config),
layer_stack: Vec::new(),
glyph_caching: false,
}
}

/// Enable or disable caching of rasterized glyphs in Vello's glyph atlas.
///
/// Defaults to `false`.
///
/// Note: Vello considers atlas-backed glyph caching experimental.
pub fn set_glyph_caching(&mut self, enabled: bool) {
self.glyph_caching = enabled;
}

fn convert_paint(&mut self, paint: PaintRef<'_>) -> PaintType {
match paint {
Paint::Solid(alpha_color) => PaintType::Solid(alpha_color),
Expand Down Expand Up @@ -196,13 +217,16 @@ impl PaintScene for VelloCpuScenePainter {
let paint = self.convert_paint(paint.into());
self.render_ctx.set_paint(paint);

let glyph_caching =
self.glyph_caching && supports_glyph_caching(transform, glyph_transform);
let style: StyleRef<'a> = style.into();
match style {
StyleRef::Fill(fill) => {
self.render_ctx.set_fill_rule(fill);
let _ = self
.render_ctx
.glyph_run(&mut self.resources, font)
.atlas_cache(glyph_caching)
.font_size(font_size)
.hint(hint)
.normalized_coords(normalized_coords)
Expand All @@ -219,6 +243,7 @@ impl PaintScene for VelloCpuScenePainter {
let _ = self
.render_ctx
.glyph_run(&mut self.resources, font)
.atlas_cache(glyph_caching)
.font_size(font_size)
.hint(hint)
.normalized_coords(normalized_coords)
Expand Down
81 changes: 81 additions & 0 deletions crates/anyrender_vello_cpu/tests/glyph_caching.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::sync::Arc;

use anyrender::{Glyph, ImageRenderer, PaintScene};
use anyrender_vello_cpu::VelloCpuImageRenderer;
use kurbo::Affine;
use peniko::{Blob, Color, Fill, FontData};

const W: u32 = 400;
const H: u32 = 160;

fn render(glyph_caching: bool, font: &FontData) -> Vec<u8> {
let mut renderer = VelloCpuImageRenderer::new(W, H);
renderer.set_glyph_caching(glyph_caching);
let mut buf = vec![0; (W * H * 4) as usize];
let skew = Affine::skew(-0.5, 0.0);
// Render twice so that the second frame can be served from the atlas.
for _ in 0..2 {
renderer.reset();
renderer.render(
|scene| {
// Upright in the top half, skewed in the bottom half.
for (y, glyph_transform) in [(60.0, None), (140.0, Some(skew))] {
scene.draw_glyphs(
font,
40.0,
true,
&[],
Default::default(),
Fill::NonZero,
Color::WHITE,
1.0,
Affine::translate((20.0, y)),
glyph_transform,
(0..8).map(|i| Glyph {
id: 40 + i,
x: i as f32 * 40.0,
y: 0.0,
}),
);
}
},
&mut buf,
);
}
buf
}

fn pixels(buf: &[u8]) -> &[[u8; 4]] {
buf.as_chunks().0
}

fn differing(a: &[u8], b: &[u8]) -> usize {
pixels(a)
.iter()
.zip(pixels(b))
.filter(|(a, b)| a.iter().zip(*b).any(|(a, b)| a.abs_diff(*b) > 64))
.count()
}

/// Vello's glyph cache key doesn't include skew/rotation, so runs it can't cache must bypass it.
#[test]
fn skewed_glyphs_are_not_drawn_from_upright_cache_entries() {
static ROBOTO: &[u8] = include_bytes!("../../../assets/fonts/roboto/Roboto.ttf");
let font = FontData::new(Blob::new(Arc::new(ROBOTO)), 0);
let off = render(false, &font);
let on = render(true, &font);
let (upright_off, skewed_off) = off.split_at(off.len() / 2);
let (upright_on, skewed_on) = on.split_at(on.len() / 2);

let ink = |buf: &[u8]| {
buf.as_chunks::<4>()
.0
.iter()
.filter(|px| px[3] > 128)
.count()
};
assert!(ink(upright_off) > 500 && ink(skewed_off) > 500);

assert_eq!(differing(upright_off, upright_on), 0);
assert_eq!(differing(skewed_off, skewed_on), 0);
}
7 changes: 7 additions & 0 deletions crates/anyrender_vello_hybrid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this crate are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Opt-in support for Vello's (experimental) glyph atlas cache, enabled at runtime via
`VelloHybridRendererOptions::glyph_caching` / `VelloHybridScenePainter::with_glyph_caching` / `WebGlScenePainter::with_glyph_caching`.

## [0.11.0] - 2026-10-03

### Added
Expand Down
26 changes: 26 additions & 0 deletions crates/anyrender_vello_hybrid/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ use wgpu_context::DeviceHandle;

const DEFAULT_TOLERANCE: f64 = 0.1;

/// Whether a glyph run drawn with the given transforms can use Vello's glyph atlas cache.
///
/// Vello only supports caching glyphs that are drawn upright with a uniform scale. It checks
/// this itself when inserting a glyph into the atlas, but not when looking one up, so a skewed
/// (e.g. synthetic italic) or rotated run would otherwise be drawn using upright cached glyphs.
pub(crate) fn supports_glyph_caching(transform: Affine, glyph_transform: Option<Affine>) -> bool {
let [a, b, c, d, _, _] = (transform * glyph_transform.unwrap_or_default()).as_coeffs();
b == 0.0 && c == 0.0 && a == d && a > 0.0
}

fn anyrender_paint_to_vello_hybrid_paint<'a>(
paint: PaintRef<'a>,
image_manager: &mut ImageManager<'_>,
Expand Down Expand Up @@ -131,6 +141,7 @@ pub struct VelloHybridScenePainter<'s> {
pub(crate) image_manager: ImageManager<'s>,
pub(crate) texture_bindings: &'s mut FxHashMap<ResourceId, TextureView>,
pub(crate) device_handle: &'s DeviceHandle,
pub(crate) glyph_caching: bool,
}

impl VelloHybridScenePainter<'_> {
Expand All @@ -146,9 +157,20 @@ impl VelloHybridScenePainter<'_> {
image_manager,
texture_bindings,
device_handle,
glyph_caching: false,
}
}

/// Enable or disable caching of rasterized glyphs in Vello's glyph atlas.
///
/// Defaults to `false`.
///
/// Note: Vello considers atlas-backed glyph caching experimental.
pub fn with_glyph_caching(mut self, enabled: bool) -> Self {
self.glyph_caching = enabled;
self
}

fn convert_paint(&mut self, paint: PaintRef<'_>) -> PaintType {
anyrender_paint_to_vello_hybrid_paint(paint, &mut self.image_manager, self.texture_bindings)
}
Expand Down Expand Up @@ -301,13 +323,16 @@ impl PaintScene for VelloHybridScenePainter<'_> {
self.scene.reset_paint_transform();
self.scene.set_transform(transform);

let glyph_caching =
self.glyph_caching && supports_glyph_caching(transform, glyph_transform);
let style: StyleRef<'a> = style.into();
match style {
StyleRef::Fill(fill) => {
self.scene.set_fill_rule(fill);
let _ = self
.scene
.glyph_run(self.image_manager.resources, font)
.atlas_cache(glyph_caching)
.font_size(font_size)
.hint(hint)
.normalized_coords(normalized_coords)
Expand All @@ -324,6 +349,7 @@ impl PaintScene for VelloHybridScenePainter<'_> {
let _ = self
.scene
.glyph_run(self.image_manager.resources, font)
.atlas_cache(glyph_caching)
.font_size(font_size)
.hint(hint)
.normalized_coords(normalized_coords)
Expand Down
16 changes: 16 additions & 0 deletions crates/anyrender_vello_hybrid/src/webgl_scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub struct WebGlScenePainter<'s> {
scene: &'s mut vello_gpu::Scene,
layer_stack: Vec<LayerKind>,
image_manager: WebGlImageManager<'s>,
glyph_caching: bool,
}

impl<'s> WebGlScenePainter<'s> {
Expand All @@ -70,8 +71,19 @@ impl<'s> WebGlScenePainter<'s> {
scene,
layer_stack: Vec::with_capacity(16),
image_manager,
glyph_caching: false,
}
}

/// Enable or disable caching of rasterized glyphs in Vello's glyph atlas.
///
/// Defaults to `false`.
///
/// Note: Vello considers atlas-backed glyph caching experimental.
pub fn with_glyph_caching(mut self, enabled: bool) -> Self {
self.glyph_caching = enabled;
self
}
}

impl WebGlScenePainter<'_> {
Expand Down Expand Up @@ -212,13 +224,16 @@ impl PaintScene for WebGlScenePainter<'_> {
self.scene.set_paint(paint);
self.scene.set_transform(transform);

let glyph_caching =
self.glyph_caching && crate::scene::supports_glyph_caching(transform, glyph_transform);
let style: StyleRef<'a> = style.into();
match style {
StyleRef::Fill(fill) => {
self.scene.set_fill_rule(fill);
let _ = self
.scene
.glyph_run(self.image_manager.resources, font)
.atlas_cache(glyph_caching)
.font_size(font_size)
.hint(hint)
.normalized_coords(normalized_coords)
Expand All @@ -235,6 +250,7 @@ impl PaintScene for WebGlScenePainter<'_> {
let _ = self
.scene
.glyph_run(self.image_manager.resources, font)
.atlas_cache(glyph_caching)
.font_size(font_size)
.hint(hint)
.normalized_coords(normalized_coords)
Expand Down
20 changes: 20 additions & 0 deletions crates/anyrender_vello_hybrid/src/window_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ pub struct VelloHybridRendererOptions {
/// lower values reduce memory usage and input latency at the cost of less
/// buffering to absorb slow frames.
pub desired_maximum_frame_latency: u32,
/// Whether to cache rasterized glyphs in Vello's glyph atlas.
///
/// Defaults to `false`.
///
/// Note: Vello considers atlas-backed glyph caching experimental.
pub glyph_caching: bool,
}

impl Default for VelloHybridRendererOptions {
Expand All @@ -82,6 +88,7 @@ impl Default for VelloHybridRendererOptions {
base_color: Color::WHITE,
composite_alpha_mode: anyrender::CompositeAlphaMode::Auto,
desired_maximum_frame_latency: 1,
glyph_caching: false,
}
}
}
Expand Down Expand Up @@ -133,6 +140,18 @@ impl VelloHybridRendererOptions {
..self
}
}

/// Enable or disable caching of rasterized glyphs in Vello's glyph atlas.
///
/// Defaults to `false`.
///
/// Note: Vello considers atlas-backed glyph caching experimental.
pub const fn glyph_caching(self, glyph_caching: bool) -> Self {
Self {
glyph_caching,
..self
}
}
}

impl From<anyrender::RendererConfig> for VelloHybridRendererOptions {
Expand Down Expand Up @@ -498,6 +517,7 @@ impl WindowRenderer for VelloHybridWindowRenderer {
image_manager,
texture_bindings: &mut state.texture_bindings,
device_handle: &render_surface.device_handle,
glyph_caching: self.config.glyph_caching,
};
if self.config.base_color != Color::TRANSPARENT {
scene_painter.fill(
Expand Down
Loading