use super::commands::AtlasCommandRecorder;
use super::key::GlyphCacheKey;
#[cfg(all(debug_assertions, feature = "std"))]
use super::key::SUBPIXEL_BUCKETS;
use super::region::{AtlasSlot, RasterMetrics};
use crate::Pixmap;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt::{Debug, Formatter};
use foldhash::fast::FixedState;
use hashbrown::HashMap;
use hashbrown::hash_map::RawEntryMut;
use smallvec::SmallVec;
pub use vello_common::image_cache::ImageCache;
pub use vello_common::multi_atlas::AtlasConfig;
use vello_common::paint::ImageId;
type FixedHashMap<K, V> = HashMap<K, V, FixedState>;
const HASH_SEED: u64 = 0;
const EMPTY_GLYPH_MAP: FixedHashMap<GlyphCacheKey, GlyphCacheEntry> =
FixedHashMap::with_hasher(FixedState::with_seed(HASH_SEED));
const EMPTY_VAR_MAP: FixedHashMap<VarKey, FixedHashMap<GlyphCacheKey, GlyphCacheEntry>> =
FixedHashMap::with_hasher(FixedState::with_seed(HASH_SEED));
pub const GLYPH_PADDING: u16 = 1;
#[derive(Clone, Debug)]
pub struct GlyphCacheConfig {
pub max_entry_age: u64,
pub eviction_frequency: u64,
pub max_cached_font_size: f32,
}
impl Default for GlyphCacheConfig {
fn default() -> Self {
Self {
max_entry_age: 64,
eviction_frequency: 64,
max_cached_font_size: 128.0,
}
}
}
#[derive(Debug)]
pub struct PendingBitmapUpload {
pub image_id: ImageId,
pub pixmap: Arc<Pixmap>,
pub atlas_slot: AtlasSlot,
}
#[derive(Clone, Copy, Debug)]
pub struct PendingClearRect {
pub page_index: u32,
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
pub struct GlyphAtlas {
eviction_config: GlyphCacheConfig,
static_entries: FixedHashMap<GlyphCacheKey, GlyphCacheEntry>,
variable_entries: FixedHashMap<VarKey, FixedHashMap<GlyphCacheKey, GlyphCacheEntry>>,
serial: u64,
last_eviction_serial: u64,
entry_count: usize,
pending_uploads: Vec<PendingBitmapUpload>,
pending_clear_rects: Vec<PendingClearRect>,
pending_atlas_commands: SmallVec<[Option<AtlasCommandRecorder>; 1]>,
cache_hits: u64,
cache_misses: u64,
}
impl GlyphAtlas {
pub fn new() -> Self {
Self::with_config(GlyphCacheConfig::default())
}
pub fn with_config(eviction_config: GlyphCacheConfig) -> Self {
Self {
eviction_config,
static_entries: EMPTY_GLYPH_MAP,
variable_entries: EMPTY_VAR_MAP,
serial: 0,
last_eviction_serial: 0,
entry_count: 0,
pending_uploads: Vec::new(),
pending_clear_rects: Vec::new(),
pending_atlas_commands: SmallVec::new(),
cache_hits: 0,
cache_misses: 0,
}
}
pub fn config(&self) -> &GlyphCacheConfig {
&self.eviction_config
}
pub fn get(&mut self, key: &GlyphCacheKey) -> Option<AtlasSlot> {
let serial = self.serial;
let entries = if key.var_coords.is_empty() {
&mut self.static_entries
} else {
match self
.variable_entries
.raw_entry_mut()
.from_key(&VarLookupKey(&key.var_coords))
{
RawEntryMut::Occupied(e) => e.into_mut(),
RawEntryMut::Vacant(_) => {
self.cache_misses += 1;
return None;
}
}
};
match entries.get_mut(key) {
Some(entry) => {
entry.serial = serial;
self.cache_hits += 1;
Some(entry.atlas_slot)
}
None => {
self.cache_misses += 1;
None
}
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "atlas offsets fit in u16 at reasonable atlas sizes"
)]
pub fn insert_entry(
&mut self,
image_cache: &mut ImageCache,
key: GlyphCacheKey,
raster_metrics: RasterMetrics,
) -> Option<AtlasSlot> {
let padded_w = u32::from(raster_metrics.width) + u32::from(GLYPH_PADDING) * 2;
let padded_h = u32::from(raster_metrics.height) + u32::from(GLYPH_PADDING) * 2;
let image_id = image_cache.allocate(padded_w, padded_h, 0).ok()?;
let resource = image_cache.get(image_id)?;
let page_index = resource.atlas_id.as_u32() as usize;
let x = resource.offset[0] + GLYPH_PADDING;
let y = resource.offset[1] + GLYPH_PADDING;
let atlas_slot = AtlasSlot {
image_id,
page_index: page_index as u32,
x,
y,
width: raster_metrics.width,
height: raster_metrics.height,
bearing_x: raster_metrics.bearing_x,
bearing_y: raster_metrics.bearing_y,
};
let entry = GlyphCacheEntry {
atlas_slot,
serial: self.serial,
};
let entries = if key.var_coords.is_empty() {
&mut self.static_entries
} else {
match self
.variable_entries
.raw_entry_mut()
.from_key(&VarLookupKey(&key.var_coords))
{
RawEntryMut::Occupied(e) => e.into_mut(),
RawEntryMut::Vacant(e) => e.insert(key.var_coords.clone(), EMPTY_GLYPH_MAP).1,
}
};
entries.insert(key, entry);
self.entry_count += 1;
Some(atlas_slot)
}
#[expect(
clippy::cast_possible_truncation,
reason = "atlas dimensions are configured to fit in u16"
)]
pub fn insert(
&mut self,
image_cache: &mut ImageCache,
key: GlyphCacheKey,
raster_metrics: RasterMetrics,
) -> Option<(AtlasSlot, &mut AtlasCommandRecorder)> {
let atlas_slot = self.insert_entry(image_cache, key, raster_metrics)?;
let (atlas_w, atlas_h) = {
let (w, h) = image_cache.atlas_manager().config().atlas_size;
(w as u16, h as u16)
};
let recorder = self.recorder_for_page(atlas_slot.page_index, atlas_w, atlas_h);
Some((atlas_slot, recorder))
}
pub fn drain_pending_uploads(&mut self) -> impl Iterator<Item = PendingBitmapUpload> + '_ {
self.pending_uploads.drain(..)
}
pub fn drain_pending_clear_rects(&mut self) -> impl Iterator<Item = PendingClearRect> + '_ {
self.pending_clear_rects.drain(..)
}
pub fn push_pending_upload(
&mut self,
image_id: ImageId,
pixmap: Arc<Pixmap>,
atlas_slot: AtlasSlot,
) {
self.pending_uploads.push(PendingBitmapUpload {
image_id,
pixmap,
atlas_slot,
});
}
pub fn replay_pending_atlas_commands(&mut self, mut f: impl FnMut(&mut AtlasCommandRecorder)) {
for slot in &mut self.pending_atlas_commands {
if let Some(recorder) = slot.as_mut()
&& !recorder.commands.is_empty()
{
f(recorder);
recorder.commands.clear();
}
}
}
pub fn recorder_for_page(
&mut self,
page_index: u32,
atlas_width: u16,
atlas_height: u16,
) -> &mut AtlasCommandRecorder {
let idx = page_index as usize;
if self.pending_atlas_commands.len() <= idx {
self.pending_atlas_commands.resize_with(idx + 1, || None);
}
self.pending_atlas_commands[idx]
.get_or_insert_with(|| AtlasCommandRecorder::new(page_index, atlas_width, atlas_height))
}
pub fn maintain(&mut self, image_cache: &mut ImageCache) {
self.tick();
let frames_since_eviction = self.serial - self.last_eviction_serial;
if frames_since_eviction < self.eviction_config.eviction_frequency {
return;
}
self.last_eviction_serial = self.serial;
self.evict_old_entries(image_cache);
}
fn tick(&mut self) {
self.serial += 1;
}
fn evict_old_entries(&mut self, image_cache: &mut ImageCache) {
let serial = self.serial;
let max_entry_age = self.eviction_config.max_entry_age;
let entry_count = &mut self.entry_count;
let pending_clear_rects = &mut self.pending_clear_rects;
let mut should_retain = |entry: &GlyphCacheEntry| -> bool {
let age = serial - entry.serial;
if age > max_entry_age {
image_cache.deallocate(entry.atlas_slot.image_id);
*entry_count = entry_count.saturating_sub(1);
push_clear_rect_for_slot(pending_clear_rects, &entry.atlas_slot);
false
} else {
true
}
};
self.static_entries.retain(|_, entry| should_retain(entry));
self.variable_entries.retain(|_, entries| {
entries.retain(|_, entry| should_retain(entry));
!entries.is_empty()
});
}
pub fn clear(&mut self) {
self.static_entries.clear();
self.variable_entries.clear();
self.serial = 0;
self.last_eviction_serial = 0;
self.entry_count = 0;
self.pending_uploads.clear();
self.pending_clear_rects.clear();
self.pending_atlas_commands.clear();
self.cache_hits = 0;
self.cache_misses = 0;
}
#[inline]
pub fn len(&self) -> usize {
self.entry_count
}
#[inline]
pub fn is_empty(&self) -> bool {
self.entry_count == 0
}
#[inline]
pub fn cache_hits(&self) -> u64 {
self.cache_hits
}
#[inline]
pub fn cache_misses(&self) -> u64 {
self.cache_misses
}
pub fn clear_stats(&mut self) {
self.cache_hits = 0;
self.cache_misses = 0;
}
}
fn push_clear_rect_for_slot(pending: &mut Vec<PendingClearRect>, slot: &AtlasSlot) {
pending.push(PendingClearRect {
page_index: slot.page_index,
x: slot.x - GLYPH_PADDING,
y: slot.y - GLYPH_PADDING,
width: slot.width + 2 * GLYPH_PADDING,
height: slot.height + 2 * GLYPH_PADDING,
});
}
#[cfg(all(debug_assertions, feature = "std"))]
#[derive(Debug)]
pub struct GlyphCacheStats {
pub static_glyphs: usize,
pub variable_glyphs: usize,
pub page_count: usize,
pub unique_glyph_ids: usize,
pub subpixel_distribution: [usize; SUBPIXEL_BUCKETS as usize],
pub sizes_used: Vec<f32>,
}
#[cfg(all(debug_assertions, feature = "std"))]
impl GlyphCacheStats {
pub fn total_glyphs(&self) -> usize {
self.static_glyphs + self.variable_glyphs
}
}
#[cfg(all(debug_assertions, feature = "std"))]
impl GlyphAtlas {
pub fn stats(&self, page_count: usize) -> GlyphCacheStats {
use std::collections::HashSet;
let mut unique_ids = HashSet::new();
let mut subpixel_dist = [0; SUBPIXEL_BUCKETS as usize];
let mut sizes = HashSet::new();
for key in self.static_entries.keys() {
unique_ids.insert(key.glyph_id);
subpixel_dist[key.subpixel_x as usize] += 1;
sizes.insert(key.size_bits);
}
let variable_count: usize = self.variable_entries.values().map(|m| m.len()).sum();
for entries in self.variable_entries.values() {
for key in entries.keys() {
unique_ids.insert(key.glyph_id);
subpixel_dist[key.subpixel_x as usize] += 1;
sizes.insert(key.size_bits);
}
}
GlyphCacheStats {
static_glyphs: self.static_entries.len(),
variable_glyphs: variable_count,
page_count,
unique_glyph_ids: unique_ids.len(),
subpixel_distribution: subpixel_dist,
sizes_used: sizes.into_iter().map(f32::from_bits).collect(),
}
}
pub fn log_hit_miss_stats(&self) {
let total = self.cache_hits + self.cache_misses;
let hit_rate = if total > 0 {
(self.cache_hits as f64 / total as f64) * 100.0
} else {
0.0
};
log::debug!("=== Cache Hit/Miss Statistics ===");
log::debug!("Cache hits: {}", self.cache_hits);
log::debug!("Cache misses: {}", self.cache_misses);
log::debug!("Total lookups: {}", total);
log::debug!("Hit rate: {:.2}%", hit_rate);
}
pub fn log_atlas_stats(&self, page_count: usize) {
let stats = self.stats(page_count);
log::debug!("=== Glyph Atlas Statistics ===");
log::debug!("Total cached glyphs: {}", stats.total_glyphs());
log::debug!("Unique glyph IDs: {}", stats.unique_glyph_ids);
log::debug!("Atlas pages: {}", stats.page_count);
log::debug!("Static font glyphs: {}", stats.static_glyphs);
log::debug!("Variable font glyphs: {}", stats.variable_glyphs);
log::debug!("Subpixel distribution: {:?}", stats.subpixel_distribution);
log::debug!("Font sizes: {:?}", stats.sizes_used);
if stats.unique_glyph_ids > 0 {
let ratio = stats.total_glyphs() as f32 / stats.unique_glyph_ids as f32;
log::debug!("Avg entries per unique glyph: {:.2}", ratio);
}
}
pub fn all_keys(&self) -> impl Iterator<Item = &GlyphCacheKey> {
self.static_entries
.keys()
.chain(self.variable_entries.values().flat_map(|e| e.keys()))
}
pub fn log_keys_grouped(&self) {
let mut by_glyph: HashMap<u32, Vec<(&GlyphCacheKey, &str)>> = HashMap::new();
for key in self.static_entries.keys() {
by_glyph
.entry(key.glyph_id)
.or_default()
.push((key, "stat"));
}
for entries in self.variable_entries.values() {
for key in entries.keys() {
by_glyph
.entry(key.glyph_id)
.or_default()
.push((key, "var "));
}
}
log::debug!(
"=== Glyph Keys Grouped by ID ({} unique) ===",
by_glyph.len()
);
let mut ids: Vec<_> = by_glyph.keys().copied().collect();
ids.sort();
for glyph_id in ids {
let keys = &by_glyph[&glyph_id];
let suffix = if keys.len() == 1 { "entry" } else { "entries" };
log::debug!("glyph_id {:4} ({} {}):", glyph_id, keys.len(), suffix);
for (k, source) in keys {
log::debug!(
" [{}] subpx: {}, size: {:.2}, hinted: {}, font_id: {:016x}, font_index: {}",
source,
k.subpixel_x,
f32::from_bits(k.size_bits),
k.hinted,
k.font_id,
k.font_index,
);
}
}
}
}
impl Default for GlyphAtlas {
fn default() -> Self {
Self::new()
}
}
impl Debug for GlyphAtlas {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GlyphAtlas")
.field("entry_count", &self.entry_count)
.field("static_entries", &self.static_entries.len())
.field("variable_fonts", &self.variable_entries.len())
.field("serial", &self.serial)
.finish_non_exhaustive()
}
}
struct GlyphCacheEntry {
atlas_slot: AtlasSlot,
serial: u64,
}
type VarKey = SmallVec<[skrifa::instance::NormalizedCoord; 4]>;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
struct VarLookupKey<'a>(&'a [skrifa::instance::NormalizedCoord]);
impl hashbrown::Equivalent<VarKey> for VarLookupKey<'_> {
fn equivalent(&self, other: &VarKey) -> bool {
self.0 == other.as_slice()
}
}
impl From<VarLookupKey<'_>> for VarKey {
fn from(key: VarLookupKey<'_>) -> Self {
Self::from_slice(key.0)
}
}