1use etagere::{size2, AllocId, Allocation, AtlasAllocator};
2use rustc_hash::FxHashMap;
3use std::fmt;
4
5use crate::{rasterize, Font, FontId, GlyphId};
6
7pub const SUBPIXEL_STEPS: u8 = 4;
9
10const PADDING: u32 = 1;
13
14const RASTER_SLACK: f32 = 2.0;
16
17const MAX_ATLAS_SIDE: u32 = 8192;
19
20const MAX_UNPLACED: usize = 4096;
23
24pub fn quantize_x(x: f32) -> (i32, u8) {
27 let steps = i32::from(SUBPIXEL_STEPS);
28 let scaled = (x * steps as f32).round() as i32;
30 (scaled.div_euclid(steps), scaled.rem_euclid(steps) as u8)
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct AtlasConfig {
41 pub page_size: u32,
43 pub max_pages: u32,
45}
46
47impl AtlasConfig {
48 pub const DEFAULT: Self = Self::new(1024, 4);
50
51 pub const fn new(page_size: u32, max_pages: u32) -> Self {
53 Self {
54 page_size,
55 max_pages,
56 }
57 }
58
59 pub fn normalized(self) -> Self {
62 let page_size = self.page_size.clamp(1, 4096);
63 Self {
64 page_size,
65 max_pages: self.max_pages.clamp(1, MAX_ATLAS_SIDE / page_size),
66 }
67 }
68}
69
70impl Default for AtlasConfig {
71 fn default() -> Self {
72 Self::DEFAULT
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct CachedGlyph {
79 pub atlas_rect: [f32; 4],
82 pub size: [f32; 2],
84 pub offset: [f32; 2],
87}
88
89impl CachedGlyph {
90 const EMPTY: Self = Self {
91 atlas_rect: [0.0; 4],
92 size: [0.0; 2],
93 offset: [0.0; 2],
94 };
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99#[non_exhaustive]
100pub enum GlyphUnavailable {
101 NoRoom,
104 TooLarge,
106 InvalidSize,
108}
109
110impl fmt::Display for GlyphUnavailable {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(match self {
113 Self::NoRoom => "no room in the glyph atlas this frame",
114 Self::TooLarge => "glyph is larger than a glyph atlas page",
115 Self::InvalidSize => "font size is not a finite, positive number of pixels",
116 })
117 }
118}
119
120impl std::error::Error for GlyphUnavailable {}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct AtlasLimitError {
125 pub max_side: u32,
127 pub needed: u32,
129}
130
131impl fmt::Display for AtlasLimitError {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(
134 f,
135 "glyph atlas needs textures of {} texels, the device allows {}",
136 self.needed, self.max_side
137 )
138 }
139}
140
141impl std::error::Error for AtlasLimitError {}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct GlyphUpload {
146 pub x: u32,
148 pub y: u32,
150 pub width: u32,
152 pub height: u32,
154 pub coverage: Vec<u8>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum AtlasUpdate {
162 Resize {
165 width: u32,
167 height: u32,
169 },
170 Write(GlyphUpload),
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
176#[non_exhaustive]
177pub struct CacheStats {
178 pub hits: u64,
180 pub misses: u64,
182 pub evictions: u64,
184 pub dropped: u64,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189struct GlyphKey {
190 font: FontId,
191 glyph: GlyphId,
192 size_64: u32,
194 subpixel: u8,
195}
196
197impl GlyphKey {
198 fn px(self) -> f32 {
200 self.size_64 as f32 / 64.0
201 }
202}
203
204#[derive(Debug)]
206struct Padded {
207 width: u32,
208 height: u32,
209 coverage: Vec<u8>,
210}
211
212#[derive(Debug)]
213enum Slot {
214 Placed { page: usize, id: AllocId },
216 NoInk,
218 Waiting {
221 padded: Padded,
222 failed_at: (u64, u64),
223 },
224 TooLarge,
226}
227
228#[derive(Debug)]
229struct Entry {
230 glyph: CachedGlyph,
232 slot: Slot,
233 last_used: u64,
235}
236
237pub struct GlyphCache {
252 config: AtlasConfig,
253 pages: Vec<AtlasAllocator>,
254 entries: FxHashMap<GlyphKey, Entry>,
255 updates: Vec<AtlasUpdate>,
256 evict_order: Vec<(u64, GlyphKey)>,
259 evict_next: usize,
260 evict_frame: u64,
261 unplaced: usize,
263 unplaced_limit: usize,
264 frame: u64,
265 room: u64,
267 generation: u64,
268 stats: CacheStats,
269}
270
271impl fmt::Debug for GlyphCache {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 f.debug_struct("GlyphCache")
275 .field("config", &self.config)
276 .field("pages", &self.pages.len())
277 .field("glyphs", &self.entries.len())
278 .field("pending_updates", &self.updates.len())
279 .field("frame", &self.frame)
280 .field("generation", &self.generation)
281 .field("stats", &self.stats)
282 .finish_non_exhaustive()
283 }
284}
285
286impl GlyphCache {
287 pub fn new(config: AtlasConfig) -> Self {
289 let config = config.normalized();
290 let mut cache = Self {
291 config,
292 pages: Vec::with_capacity(config.max_pages as usize),
293 entries: FxHashMap::default(),
294 updates: Vec::new(),
295 evict_order: Vec::new(),
296 evict_next: 0,
297 evict_frame: 0,
298 unplaced: 0,
299 unplaced_limit: MAX_UNPLACED,
300 frame: 0,
301 room: 0,
302 generation: 0,
303 stats: CacheStats::default(),
304 };
305 cache.add_page();
306 cache
307 }
308
309 pub fn config(&self) -> AtlasConfig {
311 self.config
312 }
313
314 pub fn limit_side(&mut self, max_side: u32) -> Result<(), AtlasLimitError> {
320 let needed = self.config.page_size.max(self.atlas_extent().1);
321 if needed > max_side {
322 return Err(AtlasLimitError { max_side, needed });
323 }
324 let pages_fitting = max_side / self.config.page_size;
325 self.config.max_pages = self.config.max_pages.min(pages_fitting);
326 Ok(())
327 }
328
329 pub fn atlas_extent(&self) -> (u32, u32) {
331 let page = self.config.page_size;
332 (page, page * self.pages.len() as u32)
334 }
335
336 pub fn cached_glyphs(&self) -> usize {
338 self.entries.len()
339 }
340
341 pub fn begin_frame(&mut self) {
343 self.frame += 1;
344 }
345
346 pub fn generation(&self) -> u64 {
349 self.generation
350 }
351
352 pub fn stats(&self) -> CacheStats {
354 self.stats
355 }
356
357 pub fn get(
364 &mut self,
365 font: &Font,
366 glyph: GlyphId,
367 px: f32,
368 subpixel: u8,
369 ) -> Result<CachedGlyph, GlyphUnavailable> {
370 if !(px.is_finite() && px > 0.0) {
371 self.stats.dropped += 1;
372 return Err(GlyphUnavailable::InvalidSize);
373 }
374 let key = GlyphKey {
375 font: font.id(),
376 glyph,
377 size_64: (px * 64.0).round() as u32,
379 subpixel: subpixel % SUBPIXEL_STEPS,
380 };
381 let room = self.room_stamp();
382 if let Some(entry) = self.entries.get_mut(&key) {
383 entry.last_used = self.frame;
384 match &entry.slot {
385 Slot::Placed { .. } => {
386 self.stats.hits += 1;
387 return Ok(entry.glyph);
388 }
389 Slot::NoInk => {
390 self.stats.hits += 1;
391 return Ok(CachedGlyph::EMPTY);
392 }
393 Slot::TooLarge => {
394 self.stats.dropped += 1;
395 return Err(GlyphUnavailable::TooLarge);
396 }
397 Slot::Waiting { failed_at, .. } if *failed_at == room => {
398 self.stats.dropped += 1;
399 return Err(GlyphUnavailable::NoRoom);
400 }
401 Slot::Waiting { .. } => {}
402 }
403 let Some(Entry {
404 glyph,
405 slot: Slot::Waiting { padded, .. },
406 ..
407 }) = self.entries.remove(&key)
408 else {
409 unreachable!("the entry was just seen waiting");
410 };
411 self.unplaced -= 1;
412 return self.place(key, glyph, padded);
413 }
414 self.stats.misses += 1;
415 self.rasterize_new(key, font)
416 }
417
418 pub fn drain_updates(&mut self) -> impl ExactSizeIterator<Item = AtlasUpdate> + '_ {
421 self.updates.drain(..)
422 }
423
424 fn room_stamp(&self) -> (u64, u64) {
427 (self.frame, self.room)
428 }
429
430 fn rasterize_new(
431 &mut self,
432 key: GlyphKey,
433 font: &Font,
434 ) -> Result<CachedGlyph, GlyphUnavailable> {
435 let px = key.px();
436 let limit = self.config.page_size.saturating_sub(2 * PADDING) as f32 + RASTER_SLACK;
440 let may_fit = match font.glyph_extent(key.glyph, px) {
441 Some([w, h]) => w <= limit && h <= limit,
442 None => 2.0 * px <= limit,
444 };
445 if !may_fit {
446 return self.too_large(key, CachedGlyph::EMPTY);
447 }
448
449 let x_offset = f32::from(key.subpixel) / f32::from(SUBPIXEL_STEPS);
450 let bitmap = rasterize(font, key.glyph, px, x_offset);
451 if bitmap.width == 0 {
452 self.insert(key, CachedGlyph::EMPTY, Slot::NoInk);
453 return Ok(CachedGlyph::EMPTY);
454 }
455 let glyph = CachedGlyph {
456 atlas_rect: [0.0; 4],
457 size: [bitmap.width as f32, bitmap.height as f32],
458 offset: [bitmap.left as f32, bitmap.top as f32],
459 };
460 let max = self.config.page_size.saturating_sub(2 * PADDING);
461 if bitmap.width > max || bitmap.height > max {
462 return self.too_large(key, glyph); }
464 let padded = pad(bitmap.width, bitmap.height, &bitmap.coverage);
465 self.place(key, glyph, padded)
466 }
467
468 fn too_large(
469 &mut self,
470 key: GlyphKey,
471 glyph: CachedGlyph,
472 ) -> Result<CachedGlyph, GlyphUnavailable> {
473 self.stats.dropped += 1;
474 self.insert(key, glyph, Slot::TooLarge);
475 Err(GlyphUnavailable::TooLarge)
476 }
477
478 fn place(
480 &mut self,
481 key: GlyphKey,
482 glyph: CachedGlyph,
483 padded: Padded,
484 ) -> Result<CachedGlyph, GlyphUnavailable> {
485 let Some((page, allocation)) = self.allocate(padded.width, padded.height) else {
486 self.stats.dropped += 1;
487 let failed_at = self.room_stamp();
488 self.insert(key, glyph, Slot::Waiting { padded, failed_at });
489 return Err(GlyphUnavailable::NoRoom);
490 };
491 let x = allocation.rectangle.min.x as u32;
494 let y = page as u32 * self.config.page_size + allocation.rectangle.min.y as u32;
495 let (inner_x, inner_y) = ((x + PADDING) as f32, (y + PADDING) as f32);
496 let placed = CachedGlyph {
497 atlas_rect: [
498 inner_x,
499 inner_y,
500 inner_x + glyph.size[0],
501 inner_y + glyph.size[1],
502 ],
503 ..glyph
504 };
505 self.updates.push(AtlasUpdate::Write(GlyphUpload {
506 x,
507 y,
508 width: padded.width,
509 height: padded.height,
510 coverage: padded.coverage,
511 }));
512 let slot = Slot::Placed {
513 page,
514 id: allocation.id,
515 };
516 self.insert(key, placed, slot);
517 Ok(placed)
518 }
519
520 fn allocate(&mut self, width: u32, height: u32) -> Option<(usize, Allocation)> {
524 let size = size2(width as i32, height as i32);
526 let fit = self
527 .pages
528 .iter_mut()
529 .enumerate()
530 .find_map(|(index, page)| page.allocate(size).map(|a| (index, a)));
531 if fit.is_some() {
532 return fit;
533 }
534 if self.pages.len() < self.config.max_pages as usize {
535 self.add_page();
536 let index = self.pages.len() - 1;
537 return self.pages[index].allocate(size).map(|a| (index, a));
539 }
540
541 if self.evict_frame != self.frame {
542 self.build_evict_order();
543 }
544 while let Some(&(_, key)) = self.evict_order.get(self.evict_next) {
545 self.evict_next += 1;
546 let Some(page) = self.evict(key) else {
547 continue; };
549 if let Some(allocation) = self.pages[page].allocate(size) {
551 return Some((page, allocation));
552 }
553 }
554 None
555 }
556
557 fn build_evict_order(&mut self) {
560 let frame = self.frame;
561 self.evict_order.clear();
562 self.evict_order.extend(
563 self.entries
564 .iter()
565 .filter(|(_, e)| e.last_used < frame && matches!(e.slot, Slot::Placed { .. }))
566 .map(|(key, e)| (e.last_used, *key)),
567 );
568 self.evict_order
569 .sort_unstable_by_key(|(last_used, _)| *last_used);
570 self.evict_next = 0;
571 self.evict_frame = frame;
572 }
573
574 fn evict(&mut self, key: GlyphKey) -> Option<usize> {
576 let entry = self.entries.get(&key)?;
577 let Slot::Placed { page, id } = entry.slot else {
578 return None;
579 };
580 if entry.last_used >= self.frame {
581 return None;
582 }
583 self.entries.remove(&key);
584 self.pages[page].deallocate(id);
585 self.stats.evictions += 1;
586 self.generation += 1;
587 self.room += 1;
588 Some(page)
589 }
590
591 fn add_page(&mut self) {
592 let side = self.config.page_size as i32;
594 self.pages.push(AtlasAllocator::new(size2(side, side)));
595 self.room += 1;
596 let (width, height) = self.atlas_extent();
597 self.updates.push(AtlasUpdate::Resize { width, height });
598 }
599
600 fn insert(&mut self, key: GlyphKey, glyph: CachedGlyph, slot: Slot) {
601 if !matches!(slot, Slot::Placed { .. }) {
602 self.unplaced += 1;
603 if self.unplaced > self.unplaced_limit {
604 self.forget_cold_unplaced();
605 }
606 }
607 let last_used = self.frame;
608 self.entries.insert(
609 key,
610 Entry {
611 glyph,
612 slot,
613 last_used,
614 },
615 );
616 }
617
618 fn forget_cold_unplaced(&mut self) {
621 let frame = self.frame;
622 self.entries
623 .retain(|_, e| matches!(e.slot, Slot::Placed { .. }) || e.last_used >= frame);
624 self.unplaced = self
625 .entries
626 .values()
627 .filter(|e| !matches!(e.slot, Slot::Placed { .. }))
628 .count()
629 + 1; self.unplaced_limit = MAX_UNPLACED.max(2 * self.unplaced);
631 }
632}
633
634fn pad(width: u32, height: u32, coverage: &[u8]) -> Padded {
636 let (padded_width, padded_height) = (width + 2 * PADDING, height + 2 * PADDING);
637 let (w, p, pw) = (width as usize, PADDING as usize, padded_width as usize);
638 let mut padded = vec![0u8; pw * padded_height as usize];
639 for (row, src) in coverage.chunks_exact(w).enumerate() {
640 let start = (row + p) * pw + p;
641 padded[start..start + w].copy_from_slice(src);
642 }
643 Padded {
644 width: padded_width,
645 height: padded_height,
646 coverage: padded,
647 }
648}