blob: 1cd9cb95ea1e2ec8df76ecb6c70da0e276233ed3 [file] [log] [blame]
// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use std::collections::HashMap;
use std::sync::Arc;
use super::{Encoding, StreamOffsets};
use peniko::{Font, Style};
use skrifa::instance::{NormalizedCoord, Size};
use skrifa::outline::{HintingInstance, HintingOptions, OutlineGlyphFormat};
use skrifa::{GlyphId, MetadataProvider, OutlineGlyphCollection};
#[derive(Default)]
pub(crate) struct GlyphCache {
free_list: Vec<Arc<Encoding>>,
map: GlyphMap,
var_map: HashMap<VarKey, GlyphMap>,
cached_count: usize,
hinting: HintCache,
serial: u64,
last_prune_serial: u64,
}
impl GlyphCache {
pub(crate) fn session<'a>(
&'a mut self,
font: &'a Font,
coords: &'a [NormalizedCoord],
size: f32,
hint: bool,
style: &'a Style,
) -> Option<GlyphCacheSession<'a>> {
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(crate) 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 {
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(crate) struct GlyphCacheSession<'a> {
free_list: &'a mut Vec<Arc<Encoding>>,
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 GlyphCacheSession<'_> {
pub(crate) fn get_or_insert(
&mut self,
glyph_id: u32,
) -> Option<(Arc<Encoding>, 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))?;
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
}
Style::Stroke(stroke) => {
let encoded_stroke = encoding_ptr.encode_stroke_style(stroke);
debug_assert!(encoded_stroke, "Stroke width is non-zero");
false
}
};
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 {
DrawSettings::unhinted(self.size, self.coords)
};
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))
}
}
#[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,
}
/// 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]>;
type GlyphMap = HashMap<GlyphKey, GlyphEntry>;
#[derive(Clone, Default)]
struct GlyphEntry {
encoding: Arc<Encoding>,
stream_sizes: StreamOffsets,
/// Last use of this entry.
serial: u64,
}
/// We keep this small to enable a simple LRU cache with a linear
/// search. Regenerating hinting data is low to medium cost so it's fine
/// to redo it occasionally.
const MAX_CACHED_HINT_INSTANCES: usize = 8;
pub(crate) struct HintKey<'a> {
font_id: u64,
font_index: u32,
outlines: &'a OutlineGlyphCollection<'a>,
size: Size,
coords: &'a [NormalizedCoord],
}
impl HintKey<'_> {
fn instance(&self) -> Option<HintingInstance> {
HintingInstance::new(self.outlines, self.size, self.coords, HINTING_OPTIONS).ok()
}
}
// TODO: We might want to expose making these configurable in future.
// However, these options are probably fine for most users.
const HINTING_OPTIONS: HintingOptions = HintingOptions {
engine: skrifa::outline::Engine::AutoFallback,
target: skrifa::outline::Target::Smooth {
mode: skrifa::outline::SmoothMode::Lcd,
symmetric_rendering: false,
preserve_linear_metrics: true,
},
};
#[derive(Default)]
struct HintCache {
// Split caches for glyf/cff because the instance type can reuse
// internal memory when reconfigured for the same format.
glyf_entries: Vec<HintEntry>,
cff_entries: Vec<HintEntry>,
serial: u64,
}
impl HintCache {
fn get(&mut self, key: &HintKey<'_>) -> Option<&HintingInstance> {
let entries = match key.outlines.format()? {
OutlineGlyphFormat::Glyf => &mut self.glyf_entries,
OutlineGlyphFormat::Cff | OutlineGlyphFormat::Cff2 => &mut self.cff_entries,
};
let (entry_ix, is_current) = find_hint_entry(entries, key)?;
let entry = entries.get_mut(entry_ix)?;
self.serial += 1;
entry.serial = self.serial;
if !is_current {
entry.font_id = key.font_id;
entry.font_index = key.font_index;
entry
.instance
.reconfigure(key.outlines, key.size, key.coords, HINTING_OPTIONS)
.ok()?;
}
Some(&entry.instance)
}
}
struct HintEntry {
font_id: u64,
font_index: u32,
instance: HintingInstance,
serial: u64,
}
fn find_hint_entry(entries: &mut Vec<HintEntry>, key: &HintKey<'_>) -> Option<(usize, bool)> {
let mut found_serial = u64::MAX;
let mut found_index = 0;
for (ix, entry) in entries.iter().enumerate() {
if entry.font_id == key.font_id
&& entry.font_index == key.font_index
&& entry.instance.size() == key.size
&& entry.instance.location().coords() == key.coords
{
return Some((ix, true));
}
if entry.serial < found_serial {
found_serial = entry.serial;
found_index = ix;
}
}
if entries.len() < MAX_CACHED_HINT_INSTANCES {
let instance = key.instance()?;
let ix = entries.len();
entries.push(HintEntry {
font_id: key.font_id,
font_index: key.font_index,
instance,
serial: 0,
});
Some((ix, true))
} else {
Some((found_index, false))
}
}