use etagere::{size2, AllocId, Allocation, AtlasAllocator};
use rustc_hash::FxHashMap;
use std::fmt;
use crate::{rasterize, Font, FontId, GlyphId};
pub const SUBPIXEL_STEPS: u8 = 4;
const PADDING: u32 = 1;
const RASTER_SLACK: f32 = 2.0;
const MAX_ATLAS_SIDE: u32 = 8192;
const MAX_UNPLACED: usize = 4096;
pub fn quantize_x(x: f32) -> (i32, u8) {
let steps = i32::from(SUBPIXEL_STEPS);
let scaled = (x * steps as f32).round() as i32;
(scaled.div_euclid(steps), scaled.rem_euclid(steps) as u8)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct AtlasConfig {
pub page_size: u32,
pub max_pages: u32,
}
impl AtlasConfig {
pub const DEFAULT: Self = Self::new(1024, 4);
pub const fn new(page_size: u32, max_pages: u32) -> Self {
Self {
page_size,
max_pages,
}
}
pub fn normalized(self) -> Self {
let page_size = self.page_size.clamp(1, 4096);
Self {
page_size,
max_pages: self.max_pages.clamp(1, MAX_ATLAS_SIDE / page_size),
}
}
}
impl Default for AtlasConfig {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CachedGlyph {
pub atlas_rect: [f32; 4],
pub size: [f32; 2],
pub offset: [f32; 2],
}
impl CachedGlyph {
const EMPTY: Self = Self {
atlas_rect: [0.0; 4],
size: [0.0; 2],
offset: [0.0; 2],
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GlyphUnavailable {
NoRoom,
TooLarge,
InvalidSize,
}
impl fmt::Display for GlyphUnavailable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::NoRoom => "no room in the glyph atlas this frame",
Self::TooLarge => "glyph is larger than a glyph atlas page",
Self::InvalidSize => "font size is not a finite, positive number of pixels",
})
}
}
impl std::error::Error for GlyphUnavailable {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtlasLimitError {
pub max_side: u32,
pub needed: u32,
}
impl fmt::Display for AtlasLimitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"glyph atlas needs textures of {} texels, the device allows {}",
self.needed, self.max_side
)
}
}
impl std::error::Error for AtlasLimitError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlyphUpload {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub coverage: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AtlasUpdate {
Resize {
width: u32,
height: u32,
},
Write(GlyphUpload),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub dropped: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct GlyphKey {
font: FontId,
glyph: GlyphId,
size_64: u32,
subpixel: u8,
}
impl GlyphKey {
fn px(self) -> f32 {
self.size_64 as f32 / 64.0
}
}
#[derive(Debug)]
struct Padded {
width: u32,
height: u32,
coverage: Vec<u8>,
}
#[derive(Debug)]
enum Slot {
Placed { page: usize, id: AllocId },
NoInk,
Waiting {
padded: Padded,
failed_at: (u64, u64),
},
TooLarge,
}
#[derive(Debug)]
struct Entry {
glyph: CachedGlyph,
slot: Slot,
last_used: u64,
}
pub struct GlyphCache {
config: AtlasConfig,
pages: Vec<AtlasAllocator>,
entries: FxHashMap<GlyphKey, Entry>,
updates: Vec<AtlasUpdate>,
evict_order: Vec<(u64, GlyphKey)>,
evict_next: usize,
evict_frame: u64,
unplaced: usize,
unplaced_limit: usize,
frame: u64,
room: u64,
generation: u64,
stats: CacheStats,
}
impl fmt::Debug for GlyphCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GlyphCache")
.field("config", &self.config)
.field("pages", &self.pages.len())
.field("glyphs", &self.entries.len())
.field("pending_updates", &self.updates.len())
.field("frame", &self.frame)
.field("generation", &self.generation)
.field("stats", &self.stats)
.finish_non_exhaustive()
}
}
impl GlyphCache {
pub fn new(config: AtlasConfig) -> Self {
let config = config.normalized();
let mut cache = Self {
config,
pages: Vec::with_capacity(config.max_pages as usize),
entries: FxHashMap::default(),
updates: Vec::new(),
evict_order: Vec::new(),
evict_next: 0,
evict_frame: 0,
unplaced: 0,
unplaced_limit: MAX_UNPLACED,
frame: 0,
room: 0,
generation: 0,
stats: CacheStats::default(),
};
cache.add_page();
cache
}
pub fn config(&self) -> AtlasConfig {
self.config
}
pub fn limit_side(&mut self, max_side: u32) -> Result<(), AtlasLimitError> {
let needed = self.config.page_size.max(self.atlas_extent().1);
if needed > max_side {
return Err(AtlasLimitError { max_side, needed });
}
let pages_fitting = max_side / self.config.page_size;
self.config.max_pages = self.config.max_pages.min(pages_fitting);
Ok(())
}
pub fn atlas_extent(&self) -> (u32, u32) {
let page = self.config.page_size;
(page, page * self.pages.len() as u32)
}
pub fn cached_glyphs(&self) -> usize {
self.entries.len()
}
pub fn begin_frame(&mut self) {
self.frame += 1;
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn stats(&self) -> CacheStats {
self.stats
}
pub fn get(
&mut self,
font: &Font,
glyph: GlyphId,
px: f32,
subpixel: u8,
) -> Result<CachedGlyph, GlyphUnavailable> {
if !(px.is_finite() && px > 0.0) {
self.stats.dropped += 1;
return Err(GlyphUnavailable::InvalidSize);
}
let key = GlyphKey {
font: font.id(),
glyph,
size_64: (px * 64.0).round() as u32,
subpixel: subpixel % SUBPIXEL_STEPS,
};
let room = self.room_stamp();
if let Some(entry) = self.entries.get_mut(&key) {
entry.last_used = self.frame;
match &entry.slot {
Slot::Placed { .. } => {
self.stats.hits += 1;
return Ok(entry.glyph);
}
Slot::NoInk => {
self.stats.hits += 1;
return Ok(CachedGlyph::EMPTY);
}
Slot::TooLarge => {
self.stats.dropped += 1;
return Err(GlyphUnavailable::TooLarge);
}
Slot::Waiting { failed_at, .. } if *failed_at == room => {
self.stats.dropped += 1;
return Err(GlyphUnavailable::NoRoom);
}
Slot::Waiting { .. } => {}
}
let Some(Entry {
glyph,
slot: Slot::Waiting { padded, .. },
..
}) = self.entries.remove(&key)
else {
unreachable!("the entry was just seen waiting");
};
self.unplaced -= 1;
return self.place(key, glyph, padded);
}
self.stats.misses += 1;
self.rasterize_new(key, font)
}
pub fn drain_updates(&mut self) -> impl ExactSizeIterator<Item = AtlasUpdate> + '_ {
self.updates.drain(..)
}
fn room_stamp(&self) -> (u64, u64) {
(self.frame, self.room)
}
fn rasterize_new(
&mut self,
key: GlyphKey,
font: &Font,
) -> Result<CachedGlyph, GlyphUnavailable> {
let px = key.px();
let limit = self.config.page_size.saturating_sub(2 * PADDING) as f32 + RASTER_SLACK;
let may_fit = match font.glyph_extent(key.glyph, px) {
Some([w, h]) => w <= limit && h <= limit,
None => 2.0 * px <= limit,
};
if !may_fit {
return self.too_large(key, CachedGlyph::EMPTY);
}
let x_offset = f32::from(key.subpixel) / f32::from(SUBPIXEL_STEPS);
let bitmap = rasterize(font, key.glyph, px, x_offset);
if bitmap.width == 0 {
self.insert(key, CachedGlyph::EMPTY, Slot::NoInk);
return Ok(CachedGlyph::EMPTY);
}
let glyph = CachedGlyph {
atlas_rect: [0.0; 4],
size: [bitmap.width as f32, bitmap.height as f32],
offset: [bitmap.left as f32, bitmap.top as f32],
};
let max = self.config.page_size.saturating_sub(2 * PADDING);
if bitmap.width > max || bitmap.height > max {
return self.too_large(key, glyph); }
let padded = pad(bitmap.width, bitmap.height, &bitmap.coverage);
self.place(key, glyph, padded)
}
fn too_large(
&mut self,
key: GlyphKey,
glyph: CachedGlyph,
) -> Result<CachedGlyph, GlyphUnavailable> {
self.stats.dropped += 1;
self.insert(key, glyph, Slot::TooLarge);
Err(GlyphUnavailable::TooLarge)
}
fn place(
&mut self,
key: GlyphKey,
glyph: CachedGlyph,
padded: Padded,
) -> Result<CachedGlyph, GlyphUnavailable> {
let Some((page, allocation)) = self.allocate(padded.width, padded.height) else {
self.stats.dropped += 1;
let failed_at = self.room_stamp();
self.insert(key, glyph, Slot::Waiting { padded, failed_at });
return Err(GlyphUnavailable::NoRoom);
};
let x = allocation.rectangle.min.x as u32;
let y = page as u32 * self.config.page_size + allocation.rectangle.min.y as u32;
let (inner_x, inner_y) = ((x + PADDING) as f32, (y + PADDING) as f32);
let placed = CachedGlyph {
atlas_rect: [
inner_x,
inner_y,
inner_x + glyph.size[0],
inner_y + glyph.size[1],
],
..glyph
};
self.updates.push(AtlasUpdate::Write(GlyphUpload {
x,
y,
width: padded.width,
height: padded.height,
coverage: padded.coverage,
}));
let slot = Slot::Placed {
page,
id: allocation.id,
};
self.insert(key, placed, slot);
Ok(placed)
}
fn allocate(&mut self, width: u32, height: u32) -> Option<(usize, Allocation)> {
let size = size2(width as i32, height as i32);
let fit = self
.pages
.iter_mut()
.enumerate()
.find_map(|(index, page)| page.allocate(size).map(|a| (index, a)));
if fit.is_some() {
return fit;
}
if self.pages.len() < self.config.max_pages as usize {
self.add_page();
let index = self.pages.len() - 1;
return self.pages[index].allocate(size).map(|a| (index, a));
}
if self.evict_frame != self.frame {
self.build_evict_order();
}
while let Some(&(_, key)) = self.evict_order.get(self.evict_next) {
self.evict_next += 1;
let Some(page) = self.evict(key) else {
continue; };
if let Some(allocation) = self.pages[page].allocate(size) {
return Some((page, allocation));
}
}
None
}
fn build_evict_order(&mut self) {
let frame = self.frame;
self.evict_order.clear();
self.evict_order.extend(
self.entries
.iter()
.filter(|(_, e)| e.last_used < frame && matches!(e.slot, Slot::Placed { .. }))
.map(|(key, e)| (e.last_used, *key)),
);
self.evict_order
.sort_unstable_by_key(|(last_used, _)| *last_used);
self.evict_next = 0;
self.evict_frame = frame;
}
fn evict(&mut self, key: GlyphKey) -> Option<usize> {
let entry = self.entries.get(&key)?;
let Slot::Placed { page, id } = entry.slot else {
return None;
};
if entry.last_used >= self.frame {
return None;
}
self.entries.remove(&key);
self.pages[page].deallocate(id);
self.stats.evictions += 1;
self.generation += 1;
self.room += 1;
Some(page)
}
fn add_page(&mut self) {
let side = self.config.page_size as i32;
self.pages.push(AtlasAllocator::new(size2(side, side)));
self.room += 1;
let (width, height) = self.atlas_extent();
self.updates.push(AtlasUpdate::Resize { width, height });
}
fn insert(&mut self, key: GlyphKey, glyph: CachedGlyph, slot: Slot) {
if !matches!(slot, Slot::Placed { .. }) {
self.unplaced += 1;
if self.unplaced > self.unplaced_limit {
self.forget_cold_unplaced();
}
}
let last_used = self.frame;
self.entries.insert(
key,
Entry {
glyph,
slot,
last_used,
},
);
}
fn forget_cold_unplaced(&mut self) {
let frame = self.frame;
self.entries
.retain(|_, e| matches!(e.slot, Slot::Placed { .. }) || e.last_used >= frame);
self.unplaced = self
.entries
.values()
.filter(|e| !matches!(e.slot, Slot::Placed { .. }))
.count()
+ 1; self.unplaced_limit = MAX_UNPLACED.max(2 * self.unplaced);
}
}
fn pad(width: u32, height: u32, coverage: &[u8]) -> Padded {
let (padded_width, padded_height) = (width + 2 * PADDING, height + 2 * PADDING);
let (w, p, pw) = (width as usize, PADDING as usize, padded_width as usize);
let mut padded = vec![0u8; pw * padded_height as usize];
for (row, src) in coverage.chunks_exact(w).enumerate() {
let start = (row + p) * pw + p;
padded[start..start + w].copy_from_slice(src);
}
Padded {
width: padded_width,
height: padded_height,
coverage: padded,
}
}