diff --git a/Cargo.lock b/Cargo.lock index a08d381d7..1462e15b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3834,6 +3834,7 @@ dependencies = [ "guillotiere", "peniko", "skrifa", + "smallvec", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b28c60dec..c44058c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ skrifa = "0.19.0" peniko = "0.1.0" futures-intrusive = "0.5.0" raw-window-handle = "0.6.0" +smallvec = "1.13.2" # NOTE: Make sure to keep this in sync with the version badge in README.md wgpu = { version = "0.19.3" } diff --git a/crates/encoding/Cargo.toml b/crates/encoding/Cargo.toml index c9032f7df..b09cbd069 100644 --- a/crates/encoding/Cargo.toml +++ b/crates/encoding/Cargo.toml @@ -27,3 +27,4 @@ bytemuck = { workspace = true } skrifa = { workspace = true, optional = true } peniko = { workspace = true } guillotiere = { version = "0.6.2", optional = true } +smallvec = { workspace = true } diff --git a/crates/encoding/src/glyph_cache.rs b/crates/encoding/src/glyph_cache.rs index 41440b406..a3e3e2ae9 100644 --- a/crates/encoding/src/glyph_cache.rs +++ b/crates/encoding/src/glyph_cache.rs @@ -2,160 +2,232 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use std::collections::HashMap; +use std::sync::Arc; use super::{Encoding, StreamOffsets}; -use peniko::kurbo::{BezPath, Shape}; -use peniko::{Fill, Style}; +use peniko::{Font, Style}; use skrifa::instance::{NormalizedCoord, Size}; -use skrifa::outline::{HintingInstance, HintingMode, LcdLayout, OutlineGlyphFormat, OutlinePen}; -use skrifa::{GlyphId, OutlineGlyphCollection}; - -#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)] -pub struct GlyphKey { - pub font_id: u64, - pub font_index: u32, - pub glyph_id: u32, - pub font_size_bits: u32, - pub hint: bool, -} +use skrifa::outline::{HintingInstance, HintingMode, LcdLayout, OutlineGlyphFormat}; +use skrifa::{GlyphId, MetadataProvider, OutlineGlyphCollection}; #[derive(Default)] pub struct GlyphCache { - pub encoding: Encoding, - glyphs: HashMap, + free_list: Vec>, + map: GlyphMap, + var_map: HashMap, + cached_count: usize, hinting: HintCache, + serial: u64, + last_prune_serial: u64, } impl GlyphCache { - pub fn clear(&mut self) { - self.encoding.reset(); - self.glyphs.clear(); - // No need to clear the hinting cache + pub fn session<'a>( + &'a mut self, + font: &'a Font, + coords: &'a [NormalizedCoord], + size: f32, + hint: bool, + style: &'a Style, + ) -> Option> { + let font_id = font.data.id(); + let font_index = font.index; + let font = skrifa::FontRef::from_index(font.data.as_ref(), font.index).ok()?; + let map = if !coords.is_empty() { + // This is still ugly in rust. Choices are: + // 1. multiple lookups in the hashmap (implemented here) + // 2. always allocate and copy the key + // 3. use unsafe + // Pick 1 bad option :( + if self.var_map.contains_key(coords) { + self.var_map.get_mut(coords).unwrap() + } else { + self.var_map.entry(coords.into()).or_default() + } + } else { + &mut self.map + }; + let outlines = font.outline_glyphs(); + let size = Size::new(size); + let hinter = if hint { + let key = HintKey { + font_id, + font_index, + outlines: &outlines, + size, + coords, + }; + self.hinting.get(&key) + } else { + None + }; + // TODO: we're ignoring dashing for now + let style_bits = match style { + Style::Fill(fill) => super::path::Style::from_fill(*fill), + Style::Stroke(stroke) => super::path::Style::from_stroke(stroke), + }; + let style_bits: [u32; 2] = bytemuck::cast(style_bits); + Some(GlyphCacheSession { + free_list: &mut self.free_list, + map, + font_id, + font_index, + coords, + size, + size_bits: size.ppem().unwrap().to_bits(), + style, + style_bits, + outlines, + hinter, + serial: self.serial, + cached_count: &mut self.cached_count, + }) } - pub fn get_or_insert( - &mut self, - outlines: &OutlineGlyphCollection, - key: GlyphKey, - style: &Style, - font_size: f32, - coords: &[NormalizedCoord], - ) -> Option { - let size = skrifa::instance::Size::new(font_size); - let is_var = !coords.is_empty(); - let encoding_cache = &mut self.encoding; - let hinting_cache = &mut self.hinting; - let mut encode_glyph = || { - let start = encoding_cache.stream_offsets(); - let fill = match style { - Style::Fill(fill) => *fill, - Style::Stroke(_) => Fill::NonZero, - }; - // Make sure each glyph gets encoded with a style. - // TODO: can probably optimize by setting style per run - encoding_cache.force_next_transform_and_style(); - encoding_cache.encode_fill_style(fill); - let mut path = encoding_cache.encode_path(true); - let outline = outlines.get(GlyphId::new(key.glyph_id as u16))?; - use skrifa::outline::DrawSettings; - let draw_settings = if key.hint { - if let Some(hint_instance) = - hinting_cache.get(&HintKey::new(outlines, &key, font_size, coords)) - { - DrawSettings::hinted(hint_instance, false) - } else { - DrawSettings::unhinted(size, coords) + pub fn maintain(&mut self) { + // Maximum number of resolve phases where we'll retain an unused glyph + const MAX_ENTRY_AGE: u64 = 64; + // Maximum number of resolve phases before we force a prune + const PRUNE_FREQUENCY: u64 = 64; + // Always prune if the cached count is greater than this value + const CACHED_COUNT_THRESHOLD: usize = 256; + // Number of encoding buffers we'll keep on the free list + const MAX_FREE_LIST_SIZE: usize = 32; + let free_list = &mut self.free_list; + let serial = self.serial; + self.serial += 1; + // Don't iterate over the whole cache every frame + if serial - self.last_prune_serial < PRUNE_FREQUENCY + && self.cached_count < CACHED_COUNT_THRESHOLD + { + return; + } + self.last_prune_serial = serial; + self.map.retain(|_, entry| { + if serial - entry.serial > MAX_ENTRY_AGE { + if free_list.len() < MAX_FREE_LIST_SIZE { + free_list.push(entry.encoding.clone()); } + self.cached_count -= 1; + false } else { - DrawSettings::unhinted(size, coords) - }; - match style { - Style::Fill(_) => { - outline.draw(draw_settings, &mut path).ok()?; - } - Style::Stroke(stroke) => { - const STROKE_TOLERANCE: f64 = 0.01; - let mut pen = BezPathPen::default(); - outline.draw(draw_settings, &mut pen).ok()?; - let stroked = peniko::kurbo::stroke( - pen.0.path_elements(STROKE_TOLERANCE), - stroke, - &Default::default(), - STROKE_TOLERANCE, - ); - path.shape(&stroked); + true + } + }); + self.var_map.retain(|_, map| { + map.retain(|_, entry| { + if serial - entry.serial > MAX_ENTRY_AGE { + if free_list.len() < MAX_FREE_LIST_SIZE { + free_list.push(entry.encoding.clone()); + } + self.cached_count -= 1; + false + } else { + true } + }); + !map.is_empty() + }); + } +} + +pub struct GlyphCacheSession<'a> { + free_list: &'a mut Vec>, + map: &'a mut GlyphMap, + font_id: u64, + font_index: u32, + coords: &'a [NormalizedCoord], + size: Size, + size_bits: u32, + style: &'a Style, + style_bits: [u32; 2], + outlines: OutlineGlyphCollection<'a>, + hinter: Option<&'a HintingInstance>, + serial: u64, + cached_count: &'a mut usize, +} + +impl<'a> GlyphCacheSession<'a> { + pub fn get_or_insert(&mut self, glyph_id: u32) -> Option<(Arc, StreamOffsets)> { + let key = GlyphKey { + font_id: self.font_id, + font_index: self.font_index, + glyph_id, + font_size_bits: self.size_bits, + style_bits: self.style_bits, + hint: self.hinter.is_some(), + }; + if let Some(entry) = self.map.get_mut(&key) { + entry.serial = self.serial; + return Some((entry.encoding.clone(), entry.stream_sizes)); + } + let outline = self.outlines.get(GlyphId::new(key.glyph_id as u16))?; + let mut encoding = self.free_list.pop().unwrap_or_default(); + let encoding_ptr = Arc::make_mut(&mut encoding); + encoding_ptr.reset(); + let is_fill = match &self.style { + Style::Fill(fill) => { + encoding_ptr.encode_fill_style(*fill); + true } - if path.finish(false) == 0 { - return None; + Style::Stroke(stroke) => { + encoding_ptr.encode_stroke_style(stroke); + false } - let end = encoding_cache.stream_offsets(); - Some(CachedRange { start, end }) }; - // For now, only cache non-zero filled, non-variable glyphs so we don't need to keep style - // as part of the key. - let range = if matches!(style, Style::Fill(Fill::NonZero)) && !is_var { - use std::collections::hash_map::Entry; - match self.glyphs.entry(key) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => *entry.insert(encode_glyph()?), + use skrifa::outline::DrawSettings; + let mut path = encoding_ptr.encode_path(is_fill); + let draw_settings = if key.hint { + if let Some(hinter) = self.hinter { + DrawSettings::hinted(hinter, false) + } else { + DrawSettings::unhinted(self.size, self.coords) } } else { - encode_glyph()? + DrawSettings::unhinted(self.size, self.coords) }; - Some(range) - } -} - -#[derive(Copy, Clone, Default, Debug)] -pub struct CachedRange { - pub start: StreamOffsets, - pub end: StreamOffsets, -} - -impl CachedRange { - pub fn len(&self) -> StreamOffsets { - StreamOffsets { - path_tags: self.end.path_tags - self.start.path_tags, - path_data: self.end.path_data - self.start.path_data, - draw_tags: self.end.draw_tags - self.start.draw_tags, - draw_data: self.end.draw_data - self.start.draw_data, - transforms: self.end.transforms - self.start.transforms, - styles: self.end.styles - self.start.styles, + outline.draw(draw_settings, &mut path).ok()?; + if path.finish(false) == 0 { + encoding_ptr.reset(); } + let stream_sizes = encoding_ptr.stream_offsets(); + self.map.insert( + key, + GlyphEntry { + encoding: encoding.clone(), + stream_sizes, + serial: self.serial, + }, + ); + *self.cached_count += 1; + Some((encoding, stream_sizes)) } } -// A wrapper newtype so we can implement the `OutlinePen` trait. -#[derive(Default)] -struct BezPathPen(BezPath); - -impl OutlinePen for BezPathPen { - fn move_to(&mut self, x: f32, y: f32) { - self.0.move_to((x as f64, y as f64)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.0.line_to((x as f64, y as f64)); - } +#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)] +struct GlyphKey { + font_id: u64, + font_index: u32, + glyph_id: u32, + font_size_bits: u32, + style_bits: [u32; 2], + hint: bool, +} - fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { - self.0 - .quad_to((cx0 as f64, cy0 as f64), (x as f64, y as f64)); - } +/// Outer level key for variable font caches. +/// +/// Inline size of 8 maximizes the internal storage of the small vec. +type VarKey = smallvec::SmallVec<[NormalizedCoord; 8]>; - fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { - self.0.curve_to( - (cx0 as f64, cy0 as f64), - (cx1 as f64, cy1 as f64), - (x as f64, y as f64), - ); - } +type GlyphMap = HashMap; - fn close(&mut self) { - self.0.close_path(); - } +#[derive(Clone, Default)] +struct GlyphEntry { + encoding: Arc, + stream_sizes: StreamOffsets, + /// Last use of this entry. + serial: u64, } /// We keep this small to enable a simple LRU cache with a linear @@ -172,21 +244,6 @@ pub struct HintKey<'a> { } impl<'a> HintKey<'a> { - fn new( - outlines: &'a OutlineGlyphCollection<'a>, - glyph_key: &GlyphKey, - size: f32, - coords: &'a [NormalizedCoord], - ) -> Self { - Self { - font_id: glyph_key.font_id, - font_index: glyph_key.font_index, - outlines, - size: Size::new(size), - coords, - } - } - fn instance(&self) -> Option { HintingInstance::new(self.outlines, self.size, self.coords, HINTING_MODE).ok() } diff --git a/crates/encoding/src/ramp_cache.rs b/crates/encoding/src/ramp_cache.rs index 957f98b02..1cf37d38f 100644 --- a/crates/encoding/src/ramp_cache.rs +++ b/crates/encoding/src/ramp_cache.rs @@ -24,7 +24,7 @@ pub struct RampCache { } impl RampCache { - pub fn advance(&mut self) { + pub fn maintain(&mut self) { self.epoch += 1; if self.map.len() > RETAINED_COUNT { self.map diff --git a/crates/encoding/src/resolve.rs b/crates/encoding/src/resolve.rs index 8228eef24..c627c203c 100644 --- a/crates/encoding/src/resolve.rs +++ b/crates/encoding/src/resolve.rs @@ -8,13 +8,13 @@ use super::{DrawTag, Encoding, PathTag, StreamOffsets, Style, Transform}; #[cfg(feature = "full")] use { super::{ - glyph_cache::{CachedRange, GlyphCache, GlyphKey}, + glyph_cache::GlyphCache, image_cache::{ImageCache, Images}, ramp_cache::{RampCache, Ramps}, }, peniko::{Extend, Image}, - skrifa::MetadataProvider, std::ops::Range, + std::sync::Arc, }; /// Layout of a packed encoding. @@ -164,7 +164,7 @@ pub fn resolve_solid_paths_only(encoding: &Encoding, packed: &mut Vec) -> La #[derive(Default)] pub struct Resolver { glyph_cache: GlyphCache, - glyph_ranges: Vec, + glyphs: Vec>, ramp_cache: RampCache, image_cache: ImageCache, pending_images: Vec, @@ -217,11 +217,9 @@ impl Resolver { data.extend_from_slice(bytemuck::cast_slice(&stream[pos..stream_offset])); pos = stream_offset; } - for glyph in &self.glyph_ranges[glyphs.clone()] { + for glyph in &self.glyphs[glyphs.clone()] { data.extend_from_slice(bytemuck::bytes_of(&PathTag::TRANSFORM)); - let glyph_data = &self.glyph_cache.encoding.path_tags - [glyph.start.path_tags..glyph.end.path_tags]; - data.extend_from_slice(bytemuck::cast_slice(glyph_data)); + data.extend_from_slice(bytemuck::cast_slice(&glyph.path_tags)); } data.extend_from_slice(bytemuck::bytes_of(&PathTag::PATH)); } @@ -248,10 +246,8 @@ impl Resolver { data.extend_from_slice(bytemuck::cast_slice(&stream[pos..stream_offset])); pos = stream_offset; } - for glyph in &self.glyph_ranges[glyphs.clone()] { - let glyph_data = &self.glyph_cache.encoding.path_data - [glyph.start.path_data..glyph.end.path_data]; - data.extend_from_slice(bytemuck::cast_slice(glyph_data)); + for glyph in &self.glyphs[glyphs.clone()] { + data.extend_from_slice(bytemuck::cast_slice(&glyph.path_data)); } } } @@ -372,10 +368,8 @@ impl Resolver { data.extend_from_slice(bytemuck::cast_slice(&stream[pos..stream_offset])); pos = stream_offset; } - for glyph in &self.glyph_ranges[glyphs.clone()] { - let glyph_data = - &self.glyph_cache.encoding.styles[glyph.start.styles..glyph.end.styles]; - data.extend_from_slice(bytemuck::cast_slice(glyph_data)); + for glyph in &self.glyphs[glyphs.clone()] { + data.extend_from_slice(bytemuck::cast_slice(&glyph.styles)); } } } @@ -383,15 +377,16 @@ impl Resolver { data.extend_from_slice(bytemuck::cast_slice(&stream[pos..])); } } + self.glyphs.clear(); layout.n_draw_objects = layout.n_paths; assert_eq!(buffer_size, data.len()); (layout, self.ramp_cache.ramps(), self.image_cache.images()) } fn resolve_patches(&mut self, encoding: &Encoding) -> StreamOffsets { - self.ramp_cache.advance(); - self.glyph_cache.clear(); - self.glyph_ranges.clear(); + self.ramp_cache.maintain(); + self.glyphs.clear(); + self.glyph_cache.maintain(); self.image_cache.clear(); self.pending_images.clear(); self.patches.clear(); @@ -414,17 +409,6 @@ impl Resolver { Patch::GlyphRun { index } => { let mut run_sizes = StreamOffsets::default(); let run = &resources.glyph_runs[*index]; - let font_id = run.font.data.id(); - let Ok(font_file) = skrifa::raw::FileRef::new(run.font.data.as_ref()) else { - continue; - }; - let font = match font_file { - skrifa::raw::FileRef::Font(font) => Some(font), - skrifa::raw::FileRef::Collection(collection) => { - collection.get(run.font.index).ok() - } - }; - let Some(font) = font else { continue }; let glyphs = &resources.glyphs[run.glyphs.clone()]; let coords = &resources.normalized_coords[run.normalized_coords.clone()]; let mut hint = run.hint; @@ -446,24 +430,21 @@ impl Resolver { hint = false; } } - let outlines = font.outline_glyphs(); - let glyph_start = self.glyph_ranges.len(); + let Some(mut session) = self + .glyph_cache + .session(&run.font, coords, font_size, hint, &run.style) + else { + continue; + }; + let glyph_start = self.glyphs.len(); for glyph in glyphs { - let key = GlyphKey { - font_id, - font_index: run.font.index, - font_size_bits: font_size.to_bits(), - glyph_id: glyph.id, - hint, + let Some((encoding, stream_sizes)) = session.get_or_insert(glyph.id) else { + continue; }; - let encoding_range = self - .glyph_cache - .get_or_insert(&outlines, key, &run.style, font_size, coords) - .unwrap_or_default(); - run_sizes.add(&encoding_range.len()); - self.glyph_ranges.push(encoding_range); + run_sizes.add(&stream_sizes); + self.glyphs.push(encoding); } - let glyph_end = self.glyph_ranges.len(); + let glyph_end = self.glyphs.len(); run_sizes.path_tags += glyphs.len() + 1; run_sizes.transforms += glyphs.len(); sizes.add(&run_sizes); diff --git a/src/lib.rs b/src/lib.rs index ed56b5ff6..c65c7f2f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,6 +117,9 @@ pub use recording::{ BufferProxy, Command, ImageFormat, ImageProxy, Recording, ResourceId, ResourceProxy, ShaderId, }; pub use shaders::FullShaders; + +#[cfg(feature = "wgpu")] +use vello_encoding::Resolver; #[cfg(feature = "wgpu")] use wgpu_engine::{ExternalResource, WgpuEngine}; @@ -190,6 +193,7 @@ pub struct Renderer { #[cfg_attr(not(feature = "hot_reload"), allow(dead_code))] options: RendererOptions, engine: WgpuEngine, + resolver: Resolver, shaders: FullShaders, blit: Option, target: Option, @@ -260,6 +264,7 @@ impl Renderer { Ok(Self { options, engine, + resolver: Resolver::new(), shaders, blit, target: None, @@ -286,7 +291,8 @@ impl Renderer { texture: &TextureView, params: &RenderParams, ) -> Result<()> { - let (recording, target) = render::render_full(scene, &self.shaders, params); + let (recording, target) = + render::render_full(scene, &mut self.resolver, &self.shaders, params); let external_resources = [ExternalResource::Image( *target.as_image().unwrap(), texture, @@ -427,7 +433,13 @@ impl Renderer { let encoding = scene.encoding(); // TODO: turn this on; the download feature interacts with CPU dispatch let robust = false; - let recording = render.render_encoding_coarse(encoding, &self.shaders, params, robust); + let recording = render.render_encoding_coarse( + encoding, + &mut self.resolver, + &self.shaders, + params, + robust, + ); let target = render.out_image(); let bump_buf = render.bump_buf(); self.engine.run_recording( diff --git a/src/render.rs b/src/render.rs index 279f1451a..ab44615c3 100644 --- a/src/render.rs +++ b/src/render.rs @@ -10,7 +10,7 @@ use crate::{AaConfig, RenderParams}; #[cfg(feature = "wgpu")] use crate::Scene; -use vello_encoding::{make_mask_lut, make_mask_lut_16, Encoding, WorkgroupSize}; +use vello_encoding::{make_mask_lut, make_mask_lut_16, Encoding, Resolver, WorkgroupSize}; /// State for a render in progress. pub struct Render { @@ -38,10 +38,11 @@ struct FineResources { #[cfg(feature = "wgpu")] pub fn render_full( scene: &Scene, + resolver: &mut Resolver, shaders: &FullShaders, params: &RenderParams, ) -> (Recording, ResourceProxy) { - render_encoding_full(scene.encoding(), shaders, params) + render_encoding_full(scene.encoding(), resolver, shaders, params) } #[cfg(feature = "wgpu")] @@ -51,11 +52,12 @@ pub fn render_full( /// implement robust dynamic memory. pub fn render_encoding_full( encoding: &Encoding, + resolver: &mut Resolver, shaders: &FullShaders, params: &RenderParams, ) -> (Recording, ResourceProxy) { let mut render = Render::new(); - let mut recording = render.render_encoding_coarse(encoding, shaders, params, false); + let mut recording = render.render_encoding_coarse(encoding, resolver, shaders, params, false); let out_image = render.out_image(); render.record_fine(shaders, &mut recording); (recording, out_image.into()) @@ -83,14 +85,13 @@ impl Render { pub fn render_encoding_coarse( &mut self, encoding: &Encoding, + resolver: &mut Resolver, shaders: &FullShaders, params: &RenderParams, robust: bool, ) -> Recording { - use vello_encoding::{RenderConfig, Resolver}; - + use vello_encoding::RenderConfig; let mut recording = Recording::default(); - let mut resolver = Resolver::new(); let mut packed = vec![]; let (layout, ramps, images) = resolver.resolve(encoding, &mut packed); let gradient_image = if ramps.height == 0 {