1use super::commands::AtlasCommandRecorder;
7use super::key::GlyphCacheKey;
8#[cfg(all(debug_assertions, feature = "std"))]
9use super::key::SUBPIXEL_BUCKETS;
10use super::region::{AtlasSlot, RasterMetrics};
11use crate::Pixmap;
12use alloc::sync::Arc;
13use alloc::vec::Vec;
14use core::fmt::{Debug, Formatter};
15use foldhash::fast::FixedState;
16use hashbrown::HashMap;
17use hashbrown::hash_map::RawEntryMut;
18use smallvec::SmallVec;
19pub use vello_common::image_cache::ImageCache;
20pub use vello_common::multi_atlas::AtlasConfig;
21use vello_common::paint::ImageId;
22
23type FixedHashMap<K, V> = HashMap<K, V, FixedState>;
30
31const HASH_SEED: u64 = 0;
33const EMPTY_GLYPH_MAP: FixedHashMap<GlyphCacheKey, GlyphCacheEntry> =
35 FixedHashMap::with_hasher(FixedState::with_seed(HASH_SEED));
36const EMPTY_VAR_MAP: FixedHashMap<VarKey, FixedHashMap<GlyphCacheKey, GlyphCacheEntry>> =
38 FixedHashMap::with_hasher(FixedState::with_seed(HASH_SEED));
39
40pub const GLYPH_PADDING: u16 = 1;
50
51#[derive(Clone, Debug)]
53pub struct GlyphCacheConfig {
54 pub max_entry_age: u64,
56 pub eviction_frequency: u64,
58 pub max_cached_font_size: f32,
62}
63
64impl Default for GlyphCacheConfig {
65 fn default() -> Self {
66 Self {
67 max_entry_age: 64,
68 eviction_frequency: 64,
69 max_cached_font_size: 128.0,
70 }
71 }
72}
73
74#[derive(Debug)]
82pub struct PendingBitmapUpload {
83 pub image_id: ImageId,
86 pub pixmap: Arc<Pixmap>,
88 pub atlas_slot: AtlasSlot,
90}
91
92#[derive(Clone, Copy, Debug)]
101pub struct PendingClearRect {
102 pub page_index: u32,
104 pub x: u16,
106 pub y: u16,
108 pub width: u16,
110 pub height: u16,
112}
113
114pub struct GlyphAtlas {
120 eviction_config: GlyphCacheConfig,
122 static_entries: FixedHashMap<GlyphCacheKey, GlyphCacheEntry>,
124 variable_entries: FixedHashMap<VarKey, FixedHashMap<GlyphCacheKey, GlyphCacheEntry>>,
126 serial: u64,
128 last_eviction_serial: u64,
130 entry_count: usize,
132 pending_uploads: Vec<PendingBitmapUpload>,
134 pending_clear_rects: Vec<PendingClearRect>,
136 pending_atlas_commands: SmallVec<[Option<AtlasCommandRecorder>; 1]>,
140 cache_hits: u64,
142 cache_misses: u64,
144}
145
146impl GlyphAtlas {
147 pub fn new() -> Self {
149 Self::with_config(GlyphCacheConfig::default())
150 }
151
152 pub fn with_config(eviction_config: GlyphCacheConfig) -> Self {
154 Self {
155 eviction_config,
156 static_entries: EMPTY_GLYPH_MAP,
157 variable_entries: EMPTY_VAR_MAP,
158 serial: 0,
159 last_eviction_serial: 0,
160 entry_count: 0,
161 pending_uploads: Vec::new(),
162 pending_clear_rects: Vec::new(),
163 pending_atlas_commands: SmallVec::new(),
164 cache_hits: 0,
165 cache_misses: 0,
166 }
167 }
168
169 pub fn config(&self) -> &GlyphCacheConfig {
171 &self.eviction_config
172 }
173
174 pub fn get(&mut self, key: &GlyphCacheKey) -> Option<AtlasSlot> {
176 let serial = self.serial;
177 let entries = if key.var_coords.is_empty() {
178 &mut self.static_entries
179 } else {
180 match self
181 .variable_entries
182 .raw_entry_mut()
183 .from_key(&VarLookupKey(&key.var_coords))
184 {
185 RawEntryMut::Occupied(e) => e.into_mut(),
186 RawEntryMut::Vacant(_) => {
187 self.cache_misses += 1;
188 return None;
189 }
190 }
191 };
192
193 match entries.get_mut(key) {
194 Some(entry) => {
195 entry.serial = serial;
196 self.cache_hits += 1;
197 Some(entry.atlas_slot)
198 }
199 None => {
200 self.cache_misses += 1;
201 None
202 }
203 }
204 }
205
206 #[expect(
210 clippy::cast_possible_truncation,
211 reason = "atlas offsets fit in u16 at reasonable atlas sizes"
212 )]
213 pub fn insert_entry(
214 &mut self,
215 image_cache: &mut ImageCache,
216 key: GlyphCacheKey,
217 raster_metrics: RasterMetrics,
218 ) -> Option<AtlasSlot> {
219 let padded_w = u32::from(raster_metrics.width) + u32::from(GLYPH_PADDING) * 2;
220 let padded_h = u32::from(raster_metrics.height) + u32::from(GLYPH_PADDING) * 2;
221
222 let image_id = image_cache.allocate(padded_w, padded_h, 0).ok()?;
223 let resource = image_cache.get(image_id)?;
224 let page_index = resource.atlas_id.as_u32() as usize;
225
226 let x = resource.offset[0] + GLYPH_PADDING;
227 let y = resource.offset[1] + GLYPH_PADDING;
228
229 let atlas_slot = AtlasSlot {
230 image_id,
231 page_index: page_index as u32,
232 x,
233 y,
234 width: raster_metrics.width,
235 height: raster_metrics.height,
236 bearing_x: raster_metrics.bearing_x,
237 bearing_y: raster_metrics.bearing_y,
238 };
239
240 let entry = GlyphCacheEntry {
241 atlas_slot,
242 serial: self.serial,
243 };
244
245 let entries = if key.var_coords.is_empty() {
246 &mut self.static_entries
247 } else {
248 match self
249 .variable_entries
250 .raw_entry_mut()
251 .from_key(&VarLookupKey(&key.var_coords))
252 {
253 RawEntryMut::Occupied(e) => e.into_mut(),
254 RawEntryMut::Vacant(e) => e.insert(key.var_coords.clone(), EMPTY_GLYPH_MAP).1,
255 }
256 };
257
258 entries.insert(key, entry);
259 self.entry_count += 1;
260
261 Some(atlas_slot)
262 }
263
264 #[expect(
266 clippy::cast_possible_truncation,
267 reason = "atlas dimensions are configured to fit in u16"
268 )]
269 pub fn insert(
270 &mut self,
271 image_cache: &mut ImageCache,
272 key: GlyphCacheKey,
273 raster_metrics: RasterMetrics,
274 ) -> Option<(AtlasSlot, &mut AtlasCommandRecorder)> {
275 let atlas_slot = self.insert_entry(image_cache, key, raster_metrics)?;
276 let (atlas_w, atlas_h) = {
277 let (w, h) = image_cache.atlas_manager().config().atlas_size;
278 (w as u16, h as u16)
279 };
280 let recorder = self.recorder_for_page(atlas_slot.page_index, atlas_w, atlas_h);
281 Some((atlas_slot, recorder))
282 }
283
284 pub fn drain_pending_uploads(&mut self) -> impl Iterator<Item = PendingBitmapUpload> + '_ {
286 self.pending_uploads.drain(..)
287 }
288
289 pub fn drain_pending_clear_rects(&mut self) -> impl Iterator<Item = PendingClearRect> + '_ {
291 self.pending_clear_rects.drain(..)
292 }
293
294 pub fn push_pending_upload(
296 &mut self,
297 image_id: ImageId,
298 pixmap: Arc<Pixmap>,
299 atlas_slot: AtlasSlot,
300 ) {
301 self.pending_uploads.push(PendingBitmapUpload {
302 image_id,
303 pixmap,
304 atlas_slot,
305 });
306 }
307
308 pub fn replay_pending_atlas_commands(&mut self, mut f: impl FnMut(&mut AtlasCommandRecorder)) {
314 for slot in &mut self.pending_atlas_commands {
315 if let Some(recorder) = slot.as_mut()
316 && !recorder.commands.is_empty()
317 {
318 f(recorder);
319 recorder.commands.clear();
320 }
321 }
322 }
323
324 pub fn recorder_for_page(
326 &mut self,
327 page_index: u32,
328 atlas_width: u16,
329 atlas_height: u16,
330 ) -> &mut AtlasCommandRecorder {
331 let idx = page_index as usize;
332 if self.pending_atlas_commands.len() <= idx {
333 self.pending_atlas_commands.resize_with(idx + 1, || None);
334 }
335 self.pending_atlas_commands[idx]
336 .get_or_insert_with(|| AtlasCommandRecorder::new(page_index, atlas_width, atlas_height))
337 }
338
339 pub fn maintain(&mut self, image_cache: &mut ImageCache) {
341 self.tick();
342 let frames_since_eviction = self.serial - self.last_eviction_serial;
343 if frames_since_eviction < self.eviction_config.eviction_frequency {
344 return;
345 }
346
347 self.last_eviction_serial = self.serial;
348 self.evict_old_entries(image_cache);
349 }
350
351 fn tick(&mut self) {
353 self.serial += 1;
354 }
355
356 fn evict_old_entries(&mut self, image_cache: &mut ImageCache) {
364 let serial = self.serial;
365 let max_entry_age = self.eviction_config.max_entry_age;
366 let entry_count = &mut self.entry_count;
367 let pending_clear_rects = &mut self.pending_clear_rects;
368
369 let mut should_retain = |entry: &GlyphCacheEntry| -> bool {
370 let age = serial - entry.serial;
371 if age > max_entry_age {
372 image_cache.deallocate(entry.atlas_slot.image_id);
373 *entry_count = entry_count.saturating_sub(1);
374 push_clear_rect_for_slot(pending_clear_rects, &entry.atlas_slot);
375 false
376 } else {
377 true
378 }
379 };
380
381 self.static_entries.retain(|_, entry| should_retain(entry));
382
383 self.variable_entries.retain(|_, entries| {
384 entries.retain(|_, entry| should_retain(entry));
385 !entries.is_empty()
386 });
387 }
388
389 pub fn clear(&mut self) {
391 self.static_entries.clear();
392 self.variable_entries.clear();
393 self.serial = 0;
394 self.last_eviction_serial = 0;
395 self.entry_count = 0;
396 self.pending_uploads.clear();
397 self.pending_clear_rects.clear();
398 self.pending_atlas_commands.clear();
399 self.cache_hits = 0;
400 self.cache_misses = 0;
401 }
402
403 #[inline]
405 pub fn len(&self) -> usize {
406 self.entry_count
407 }
408
409 #[inline]
411 pub fn is_empty(&self) -> bool {
412 self.entry_count == 0
413 }
414
415 #[inline]
417 pub fn cache_hits(&self) -> u64 {
418 self.cache_hits
419 }
420
421 #[inline]
423 pub fn cache_misses(&self) -> u64 {
424 self.cache_misses
425 }
426
427 pub fn clear_stats(&mut self) {
429 self.cache_hits = 0;
430 self.cache_misses = 0;
431 }
432}
433
434fn push_clear_rect_for_slot(pending: &mut Vec<PendingClearRect>, slot: &AtlasSlot) {
440 pending.push(PendingClearRect {
441 page_index: slot.page_index,
442 x: slot.x - GLYPH_PADDING,
443 y: slot.y - GLYPH_PADDING,
444 width: slot.width + 2 * GLYPH_PADDING,
445 height: slot.height + 2 * GLYPH_PADDING,
446 });
447}
448
449#[cfg(all(debug_assertions, feature = "std"))]
451#[derive(Debug)]
452pub struct GlyphCacheStats {
453 pub static_glyphs: usize,
455 pub variable_glyphs: usize,
457 pub page_count: usize,
459 pub unique_glyph_ids: usize,
461 pub subpixel_distribution: [usize; SUBPIXEL_BUCKETS as usize],
463 pub sizes_used: Vec<f32>,
465}
466
467#[cfg(all(debug_assertions, feature = "std"))]
468impl GlyphCacheStats {
469 pub fn total_glyphs(&self) -> usize {
471 self.static_glyphs + self.variable_glyphs
472 }
473}
474
475#[cfg(all(debug_assertions, feature = "std"))]
476impl GlyphAtlas {
477 pub fn stats(&self, page_count: usize) -> GlyphCacheStats {
479 use std::collections::HashSet;
480
481 let mut unique_ids = HashSet::new();
482 let mut subpixel_dist = [0; SUBPIXEL_BUCKETS as usize];
483 let mut sizes = HashSet::new();
484
485 for key in self.static_entries.keys() {
486 unique_ids.insert(key.glyph_id);
487 subpixel_dist[key.subpixel_x as usize] += 1;
488 sizes.insert(key.size_bits);
489 }
490
491 let variable_count: usize = self.variable_entries.values().map(|m| m.len()).sum();
492
493 for entries in self.variable_entries.values() {
494 for key in entries.keys() {
495 unique_ids.insert(key.glyph_id);
496 subpixel_dist[key.subpixel_x as usize] += 1;
497 sizes.insert(key.size_bits);
498 }
499 }
500
501 GlyphCacheStats {
502 static_glyphs: self.static_entries.len(),
503 variable_glyphs: variable_count,
504 page_count,
505 unique_glyph_ids: unique_ids.len(),
506 subpixel_distribution: subpixel_dist,
507 sizes_used: sizes.into_iter().map(f32::from_bits).collect(),
508 }
509 }
510
511 pub fn log_hit_miss_stats(&self) {
513 let total = self.cache_hits + self.cache_misses;
514 let hit_rate = if total > 0 {
515 (self.cache_hits as f64 / total as f64) * 100.0
516 } else {
517 0.0
518 };
519 log::debug!("=== Cache Hit/Miss Statistics ===");
520 log::debug!("Cache hits: {}", self.cache_hits);
521 log::debug!("Cache misses: {}", self.cache_misses);
522 log::debug!("Total lookups: {}", total);
523 log::debug!("Hit rate: {:.2}%", hit_rate);
524 }
525
526 pub fn log_atlas_stats(&self, page_count: usize) {
528 let stats = self.stats(page_count);
529 log::debug!("=== Glyph Atlas Statistics ===");
530 log::debug!("Total cached glyphs: {}", stats.total_glyphs());
531 log::debug!("Unique glyph IDs: {}", stats.unique_glyph_ids);
532 log::debug!("Atlas pages: {}", stats.page_count);
533 log::debug!("Static font glyphs: {}", stats.static_glyphs);
534 log::debug!("Variable font glyphs: {}", stats.variable_glyphs);
535 log::debug!("Subpixel distribution: {:?}", stats.subpixel_distribution);
536 log::debug!("Font sizes: {:?}", stats.sizes_used);
537
538 if stats.unique_glyph_ids > 0 {
539 let ratio = stats.total_glyphs() as f32 / stats.unique_glyph_ids as f32;
540 log::debug!("Avg entries per unique glyph: {:.2}", ratio);
541 }
542 }
543
544 pub fn all_keys(&self) -> impl Iterator<Item = &GlyphCacheKey> {
546 self.static_entries
547 .keys()
548 .chain(self.variable_entries.values().flat_map(|e| e.keys()))
549 }
550
551 pub fn log_keys_grouped(&self) {
556 let mut by_glyph: HashMap<u32, Vec<(&GlyphCacheKey, &str)>> = HashMap::new();
557
558 for key in self.static_entries.keys() {
559 by_glyph
560 .entry(key.glyph_id)
561 .or_default()
562 .push((key, "stat"));
563 }
564 for entries in self.variable_entries.values() {
565 for key in entries.keys() {
566 by_glyph
567 .entry(key.glyph_id)
568 .or_default()
569 .push((key, "var "));
570 }
571 }
572
573 log::debug!(
574 "=== Glyph Keys Grouped by ID ({} unique) ===",
575 by_glyph.len()
576 );
577
578 let mut ids: Vec<_> = by_glyph.keys().copied().collect();
579 ids.sort();
580
581 for glyph_id in ids {
582 let keys = &by_glyph[&glyph_id];
583 let suffix = if keys.len() == 1 { "entry" } else { "entries" };
584 log::debug!("glyph_id {:4} ({} {}):", glyph_id, keys.len(), suffix);
585 for (k, source) in keys {
586 log::debug!(
587 " [{}] subpx: {}, size: {:.2}, hinted: {}, font_id: {:016x}, font_index: {}",
588 source,
589 k.subpixel_x,
590 f32::from_bits(k.size_bits),
591 k.hinted,
592 k.font_id,
593 k.font_index,
594 );
595 }
596 }
597 }
598}
599
600impl Default for GlyphAtlas {
601 fn default() -> Self {
602 Self::new()
603 }
604}
605
606impl Debug for GlyphAtlas {
607 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
608 f.debug_struct("GlyphAtlas")
609 .field("entry_count", &self.entry_count)
610 .field("static_entries", &self.static_entries.len())
611 .field("variable_fonts", &self.variable_entries.len())
612 .field("serial", &self.serial)
613 .finish_non_exhaustive()
614 }
615}
616
617struct GlyphCacheEntry {
619 atlas_slot: AtlasSlot,
621 serial: u64,
623}
624
625type VarKey = SmallVec<[skrifa::instance::NormalizedCoord; 4]>;
627
628#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
630struct VarLookupKey<'a>(&'a [skrifa::instance::NormalizedCoord]);
631
632impl hashbrown::Equivalent<VarKey> for VarLookupKey<'_> {
633 fn equivalent(&self, other: &VarKey) -> bool {
634 self.0 == other.as_slice()
635 }
636}
637
638impl From<VarLookupKey<'_>> for VarKey {
639 fn from(key: VarLookupKey<'_>) -> Self {
640 Self::from_slice(key.0)
641 }
642}