| // Copyright 2025 the Vello Authors and the Parley Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! Vello CPU glyph rendering backend. | |
| //! | |
| //! Provides [`GlyphAtlas`] (atlas backed by per-page [`Pixmap`]s) and the | |
| //! [`GlyphRenderer`](glifo::GlyphRenderer) implementation for [`RenderContext`] that | |
| //! rasterises glyphs into CPU-accessible pixel buffers. | |
| //! | |
| //! The key difference from the GPU backend is that atlas pages are owned as | |
| //! [`Arc<Pixmap>`]s here, so the CPU renderer can read pixels directly without | |
| //! any GPU upload step. | |
| use crate::render::{ATLAS_IMAGE_ID_BASE, DEFAULT_GLYPH_ATLAS_SIZE}; | |
| use crate::{ | |
| Image, ImageSource, PaintType, Pixmap, RasterizerSettings, RenderContext, RenderMode, | |
| RenderSettings, Resources, TargetInit, color, kurbo, peniko, | |
| }; | |
| use alloc::boxed::Box; | |
| use alloc::sync::Arc; | |
| use alloc::vec::Vec; | |
| use color::palette::css::BLACK; | |
| use core::fmt::Debug; | |
| use core::ops::RangeInclusive; | |
| use glifo::atlas::{ | |
| AtlasConfig, AtlasSlot, GlyphAtlas, GlyphCacheConfig, ImageCache, PendingClearRect, | |
| }; | |
| use glifo::{AtlasCacher, DrawSink, GlyphRunBackend}; | |
| use glifo::{Glyph, renderer}; | |
| use kurbo::{Affine, BezPath, Rect}; | |
| use peniko::BlendMode; | |
| use peniko::color::{AlphaColor, Srgb}; | |
| use vello_common::fearless_simd::Level; | |
| use vello_common::paint::ImageId; | |
| fn atlas_page_image_id(page_index: u32) -> ImageId { | |
| ImageId::new(ATLAS_IMAGE_ID_BASE + page_index) | |
| } | |
| #[derive(Debug)] | |
| pub(crate) struct GlyphAtlasResources { | |
| pub(crate) glyph_atlas: GlyphAtlas, | |
| pub(crate) image_cache: ImageCache, | |
| pub(crate) glyph_renderer: Box<RenderContext>, | |
| /// One `Pixmap` per atlas page, grown on demand. | |
| // It's a bit annoying to have this in an `Arc`, but it needs to be this way. During fine | |
| // rasterization, we need to be able to easily clone the atlas page so that it can be shared | |
| // across multiple threads. However, we also need to be able to mutate the pixmap to | |
| // sync new glyphs. The way this is achieved is by calling `Arc::get_mut` when syncing | |
| // (at this point, the pixmap isn't shared anywhere else). Before fine rasterization, we | |
| // then share it with the image registry such that it can easily be fetched and cloned during | |
| // fine rasterization. After that, we remove it from the image registry, such that it's uniquely | |
| // owned again and can be mutated in the next frame. | |
| pub(crate) pixmaps: Vec<Arc<Pixmap>>, | |
| /// Width of each atlas page in pixels. | |
| page_width: u16, | |
| /// Height of each atlas page in pixels. | |
| page_height: u16, | |
| } | |
| impl GlyphAtlasResources { | |
| pub(crate) fn with_config( | |
| page_width: u16, | |
| page_height: u16, | |
| level: Level, | |
| eviction_config: GlyphCacheConfig, | |
| ) -> Self { | |
| Self { | |
| glyph_atlas: GlyphAtlas::with_config(eviction_config), | |
| image_cache: ImageCache::new_with_config(AtlasConfig::default()), | |
| glyph_renderer: Box::new(RenderContext::new_with( | |
| page_width, | |
| page_height, | |
| RenderSettings { | |
| level, | |
| num_threads: 0, | |
| }, | |
| )), | |
| pixmaps: Vec::new(), | |
| page_width, | |
| page_height, | |
| } | |
| } | |
| pub(crate) fn maintain(&mut self) { | |
| self.glyph_atlas.maintain(&mut self.image_cache); | |
| } | |
| } | |
| /// Ensure a pixmap exists for the given page, creating it if needed. | |
| fn ensure_page( | |
| pixmaps: &mut Vec<Arc<Pixmap>>, | |
| page_width: u16, | |
| page_height: u16, | |
| page_index: usize, | |
| ) { | |
| while pixmaps.len() <= page_index { | |
| pixmaps.push(Arc::new(Pixmap::new(page_width, page_height))); | |
| } | |
| } | |
| impl Resources { | |
| pub(crate) fn prepare_glyph_cache(&mut self, render_mode: RenderMode) { | |
| if self.glyph_resources.is_some() { | |
| self.sync_glyph_cache(render_mode); | |
| } | |
| } | |
| pub(crate) fn maintain_glyph_cache(&mut self) { | |
| self.glyph_prep_cache.maintain(); | |
| if let Some(glyph_resources) = self.glyph_resources.as_mut() { | |
| glyph_resources.maintain(); | |
| let page_count = glyph_resources.pixmaps.len(); | |
| for page_index in 0..page_count { | |
| self.image_registry.destroy_atlas_page(page_index as u32); | |
| } | |
| self.clear_evicted_glyph_atlas_regions(); | |
| } | |
| } | |
| fn ensure_glyph_resources(&mut self, level: Level) { | |
| if self.glyph_resources.is_none() { | |
| self.glyph_resources = Some(GlyphAtlasResources::with_config( | |
| DEFAULT_GLYPH_ATLAS_SIZE, | |
| DEFAULT_GLYPH_ATLAS_SIZE, | |
| level, | |
| GlyphCacheConfig::default(), | |
| )); | |
| } | |
| } | |
| /// Upload all pending bitmaps, rasterize pending outline/COLR glyphs, etc. | |
| fn sync_glyph_cache(&mut self, render_mode: RenderMode) { | |
| let glyph_resources = self | |
| .glyph_resources | |
| .as_mut() | |
| .expect("glyph atlas resources must exist before syncing"); | |
| // Upload all pending bitmap glyphs to the image atlas. | |
| for upload in glyph_resources.glyph_atlas.drain_pending_uploads() { | |
| let page_index = upload.atlas_slot.page_index as usize; | |
| ensure_page( | |
| &mut glyph_resources.pixmaps, | |
| glyph_resources.page_width, | |
| glyph_resources.page_height, | |
| page_index, | |
| ); | |
| let pixmap = Arc::get_mut(&mut glyph_resources.pixmaps[page_index]) | |
| .expect("atlas pixmap should be uniquely owned during bitmap upload"); | |
| copy_pixmap_to_atlas( | |
| &upload.pixmap, | |
| pixmap, | |
| upload.atlas_slot.x, | |
| upload.atlas_slot.y, | |
| upload.atlas_slot.width, | |
| upload.atlas_slot.height, | |
| ); | |
| } | |
| // Draw all new COLR/outline glyphs into the render context, and then composite them into the | |
| // existing atlas page. | |
| let glyph_renderer = glyph_resources.glyph_renderer.as_mut(); | |
| glyph_resources | |
| .glyph_atlas | |
| .replay_pending_atlas_commands(|recorder| { | |
| let page_index = recorder.page_index as usize; | |
| ensure_page( | |
| &mut glyph_resources.pixmaps, | |
| glyph_resources.page_width, | |
| glyph_resources.page_height, | |
| page_index, | |
| ); | |
| let page = Arc::get_mut(&mut glyph_resources.pixmaps[page_index]) | |
| .expect("atlas page pixmap must be uniquely owned during replay"); | |
| glyph_renderer.reset(); | |
| renderer::replay_atlas_commands(&mut recorder.commands, glyph_renderer); | |
| glyph_renderer.flush(); | |
| glyph_renderer.render_with( | |
| page, | |
| &mut Self::default(), | |
| RasterizerSettings { | |
| render_mode, | |
| target_init: TargetInit::SrcOver, | |
| ..Default::default() | |
| }, | |
| ); | |
| Ok::<(), core::convert::Infallible>(()) | |
| }) | |
| .unwrap_or_else(|error| match error {}); | |
| for (page_index, pixmap) in glyph_resources.pixmaps.iter().enumerate() { | |
| self.image_registry | |
| .register_atlas_page(page_index as u32, Arc::clone(pixmap)); | |
| } | |
| } | |
| fn clear_evicted_glyph_atlas_regions(&mut self) { | |
| let glyph_resources = self | |
| .glyph_resources | |
| .as_mut() | |
| .expect("glyph atlas resources must exist before clearing"); | |
| for clear in glyph_resources.glyph_atlas.drain_pending_clear_rects() { | |
| let pixmap = Arc::get_mut(&mut glyph_resources.pixmaps[clear.page_index as usize]) | |
| .expect("atlas pixmap should be uniquely owned during region clearing"); | |
| clear_pixmap_region(pixmap, clear); | |
| } | |
| } | |
| } | |
| #[doc(hidden)] | |
| #[derive(Debug)] | |
| pub struct CpuGlyphRunBackend<'a> { | |
| pub ctx: &'a mut RenderContext, | |
| pub resources: &'a mut Resources, | |
| pub atlas_cache_enabled: bool, | |
| } | |
| impl<'a> CpuGlyphRunBackend<'a> { | |
| fn render_glyphs<Glyphs, R>( | |
| self, | |
| run: glifo::GlyphRun<'a>, | |
| glyphs: Glyphs, | |
| render: impl FnOnce(&mut glifo::GlyphRunRenderer<'a, 'a, Glyphs>, &mut RenderContext) -> R, | |
| ) -> R | |
| where | |
| Glyphs: Iterator<Item = Glyph> + Clone, | |
| { | |
| let atlas_cacher = if self.atlas_cache_enabled { | |
| self.resources | |
| .ensure_glyph_resources(self.ctx.render_settings.level); | |
| 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 glyph_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.ctx) | |
| } | |
| } | |
| impl<'a> GlyphRunBackend<'a> for CpuGlyphRunBackend<'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, ctx| glyph_run.fill_glyphs(ctx)) | |
| } | |
| 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, ctx| { | |
| let stroke_adjustment = glyph_run.stroke_adjustment(); | |
| let original_width = ctx.stroke().width; | |
| ctx.stroke_mut().width *= stroke_adjustment; | |
| let result = glyph_run.stroke_glyphs(ctx); | |
| ctx.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, ctx| { | |
| glyph_run.render_decoration(x_range, baseline_y, offset, size, buffer, ctx); | |
| }); | |
| } | |
| } | |
| /// A glyph run builder. | |
| pub type GlyphRunBuilder<'a> = glifo::GlyphRunBuilder<'a, CpuGlyphRunBackend<'a>>; | |
| /// Zero out a rectangular region in the atlas pixmap. | |
| /// | |
| /// Necessary because atlas rendering uses `SrcOver` blending, | |
| /// so stale pixels from evicted glyphs would bleed through if not cleared. | |
| fn clear_pixmap_region(dst: &mut Pixmap, rect: PendingClearRect) { | |
| let dst_stride = dst.width() as usize; | |
| let dst_data = dst.data_as_u8_slice_mut(); | |
| let clear_width = rect.width as usize; | |
| let clear_height = rect.height as usize; | |
| for y in 0..clear_height { | |
| let row_start = ((rect.y as usize + y) * dst_stride + rect.x as usize) * 4; | |
| let row_end = row_start + clear_width * 4; | |
| dst_data[row_start..row_end].fill(0); | |
| } | |
| } | |
| /// Copy bitmap glyph pixels into a rectangular region of an atlas page. | |
| fn copy_pixmap_to_atlas( | |
| src: &Pixmap, | |
| dst: &mut Pixmap, | |
| dst_x: u16, | |
| dst_y: u16, | |
| width: u16, | |
| height: u16, | |
| ) { | |
| let copy_width = width as usize; | |
| let copy_height = height as usize; | |
| let src_stride = src.width() as usize; | |
| let dst_stride = dst.width() as usize; | |
| let src_data = src.data_as_u8_slice(); | |
| let dst_data = dst.data_as_u8_slice_mut(); | |
| for y in 0..copy_height { | |
| let src_row_start = y * src_stride * 4; | |
| let src_row_end = src_row_start + copy_width * 4; | |
| let dst_row_start = ((dst_y as usize + y) * dst_stride + dst_x as usize) * 4; | |
| let dst_row_end = dst_row_start + copy_width * 4; | |
| dst_data[dst_row_start..dst_row_end].copy_from_slice(&src_data[src_row_start..src_row_end]); | |
| } | |
| } | |
| impl DrawSink for RenderContext { | |
| #[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) | |
| } | |
| } | |
| impl glifo::GlyphRenderer for RenderContext { | |
| 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_page_image_id(atlas_slot.page_index)) | |
| } | |
| #[inline] | |
| fn atlas_paint_transform(&self, atlas_slot: &AtlasSlot) -> Affine { | |
| Affine::translate((-(atlas_slot.x as f64), -(atlas_slot.y as f64))) | |
| } | |
| } | |
| /// Debug utilities for visualizing glyph bounds during rasterization. | |
| #[cfg(debug_assertions)] | |
| #[allow( | |
| dead_code, | |
| unreachable_pub, | |
| clippy::trivially_copy_pass_by_ref, | |
| reason = "debug-only utilities called manually during development" | |
| )] | |
| mod debug { | |
| use core::sync::atomic::{AtomicUsize, Ordering}; | |
| use crate::RenderContext; | |
| use crate::kurbo::{Affine, Rect}; | |
| use crate::peniko; | |
| use glifo::atlas::RasterMetrics; | |
| static COLOR_INDEX: AtomicUsize = AtomicUsize::new(0); | |
| /// Twelve semi-transparent rotating colours for distinguishing adjacent | |
| /// glyph bounds. Each call to [`fill_glyph_bounds`] advances the index. | |
| const COLORS: [peniko::Color; 12] = [ | |
| peniko::Color::new([1.0, 0.0, 0.0, 0.5]), // Red | |
| peniko::Color::new([0.0, 1.0, 0.0, 0.5]), // Green | |
| peniko::Color::new([0.0, 0.0, 1.0, 0.5]), // Blue | |
| peniko::Color::new([1.0, 1.0, 0.0, 0.5]), // Yellow | |
| peniko::Color::new([1.0, 0.0, 1.0, 0.5]), // Magenta | |
| peniko::Color::new([0.0, 1.0, 1.0, 0.5]), // Cyan | |
| peniko::Color::new([1.0, 0.5, 0.0, 0.5]), // Orange | |
| peniko::Color::new([0.5, 0.0, 1.0, 0.5]), // Purple | |
| peniko::Color::new([0.0, 1.0, 0.5, 0.5]), // Mint | |
| peniko::Color::new([1.0, 0.5, 0.5, 0.5]), // Pink | |
| peniko::Color::new([0.5, 1.0, 0.5, 0.5]), // Light green | |
| peniko::Color::new([0.5, 0.5, 1.0, 0.5]), // Light blue | |
| ]; | |
| /// Fill the glyph's pixel bounds with the next rotating debug colour. | |
| /// Call before rendering the actual glyph content to visualise the extent. | |
| pub fn fill_glyph_bounds(renderer: &mut RenderContext, raster_metrics: &RasterMetrics) { | |
| let idx = COLOR_INDEX.fetch_add(1, Ordering::Relaxed) % COLORS.len(); | |
| renderer.set_transform(Affine::IDENTITY); | |
| renderer.set_paint(COLORS[idx]); | |
| renderer.fill_rect(&Rect::new( | |
| 0.0, | |
| 0.0, | |
| raster_metrics.width as f64, | |
| raster_metrics.height as f64, | |
| )); | |
| } | |
| } |