use alloc::collections::BTreeMap;
use alloc::vec;
use alloc::vec::Vec;
use denise::{AtlasPage, Mask, Rect, Size};
use core::sync::atomic::{AtomicU64, Ordering};
use crate::source::{FontId, GlyphId, GlyphMetrics, GlyphSource};
static NEXT_ATLAS: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GlyphKey {
pub font: FontId,
pub size_px: u16,
pub glyph: GlyphId,
}
impl GlyphKey {
pub const fn from_char(font: FontId, size_px: u16, ch: char) -> Self {
Self {
font,
size_px,
glyph: GlyphId::from_char(ch),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Placed {
pub rect: Rect,
pub metrics: GlyphMetrics,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AtlasStats {
pub hits: u64,
pub misses: u64,
pub resets: u64,
pub failures: u64,
}
impl AtlasStats {
pub fn hit_rate(&self) -> Option<u32> {
let total = self.hits + self.misses;
(total > 0).then(|| (self.hits * 100 / total) as u32)
}
}
#[derive(Clone, Copy, Debug)]
struct Shelf {
y: u32,
height: u32,
next_x: u32,
}
pub struct GlyphAtlas {
id: u64,
version: u64,
coverage: Vec<u8>,
size: Size,
shelves: Vec<Shelf>,
used_height: u32,
entries: BTreeMap<GlyphKey, Placed>,
stats: AtlasStats,
}
impl GlyphAtlas {
pub fn new(size: Size) -> Self {
let size = Size::new(size.width.max(1), size.height.max(1));
Self {
id: NEXT_ATLAS.fetch_add(1, Ordering::Relaxed),
version: 0,
coverage: vec![0; size.area() as usize],
size,
shelves: Vec::new(),
used_height: 0,
entries: BTreeMap::new(),
stats: AtlasStats::default(),
}
}
pub fn with_default_size() -> Self {
Self::new(Size::new(256, 256))
}
#[inline]
pub const fn size(&self) -> Size {
self.size
}
#[inline]
pub fn capacity_bytes(&self) -> usize {
self.coverage.len()
}
#[inline]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[inline]
pub const fn stats(&self) -> AtlasStats {
self.stats
}
pub fn clear(&mut self) {
self.version = self.version.wrapping_add(1);
self.entries.clear();
self.shelves.clear();
self.used_height = 0;
}
pub fn get_or_insert(&mut self, key: GlyphKey, source: &mut dyn GlyphSource) -> Option<Placed> {
if let Some(placed) = self.entries.get(&key) {
self.stats.hits += 1;
return Some(*placed);
}
self.stats.misses += 1;
let Some(glyph) = source.rasterise(key.glyph, key.size_px) else {
self.stats.failures += 1;
return None;
};
let metrics = glyph.metrics;
if metrics.is_blank() {
let placed = Placed {
rect: Rect::ZERO,
metrics,
};
self.entries.insert(key, placed);
return Some(placed);
}
let width = metrics.size.width;
let height = metrics.size.height;
let rect = match self.pack(width, height) {
Some(rect) => rect,
None => {
self.clear();
self.stats.resets += 1;
self.pack(width, height).or_else(|| {
self.stats.failures += 1;
None
})?
}
};
for row in 0..height as usize {
let from = row * glyph.stride;
let to = (rect.y as usize + row) * self.size.width as usize + rect.x as usize;
self.coverage[to..to + width as usize]
.copy_from_slice(&glyph.coverage[from..from + width as usize]);
}
self.version = self.version.wrapping_add(1);
let placed = Placed { rect, metrics };
self.entries.insert(key, placed);
Some(placed)
}
#[inline]
pub const fn id(&self) -> u64 {
self.id
}
#[inline]
pub const fn version(&self) -> u64 {
self.version
}
pub fn page(&self) -> AtlasPage<'_> {
AtlasPage {
id: self.id,
version: self.version,
mask: Mask::new(
&self.coverage,
self.size.width as i32,
self.size.height as i32,
self.size.width as usize,
)
.expect("the page is exactly its own size"),
}
}
pub fn mask(&self, placed: &Placed) -> Option<Mask<'_>> {
if placed.rect.is_empty() {
return None;
}
let offset = placed.rect.y as usize * self.size.width as usize + placed.rect.x as usize;
Mask::new(
&self.coverage[offset..],
placed.rect.width,
placed.rect.height,
self.size.width as usize,
)
}
fn pack(&mut self, width: u32, height: u32) -> Option<Rect> {
if width > self.size.width || height > self.size.height {
return None;
}
let mut best: Option<usize> = None;
for (index, shelf) in self.shelves.iter().enumerate() {
if shelf.height < height || shelf.next_x + width > self.size.width {
continue;
}
let better = best.is_none_or(|b| shelf.height < self.shelves[b].height);
if better {
best = Some(index);
}
}
if let Some(index) = best {
let shelf = &mut self.shelves[index];
let rect = Rect::new(
shelf.next_x as i32,
shelf.y as i32,
width as i32,
height as i32,
);
shelf.next_x += width;
return Some(rect);
}
if self.used_height + height > self.size.height {
return None;
}
let shelf = Shelf {
y: self.used_height,
height,
next_x: width,
};
let rect = Rect::new(0, shelf.y as i32, width as i32, height as i32);
self.used_height += height;
self.shelves.push(shelf);
Some(rect)
}
}
impl core::fmt::Debug for GlyphAtlas {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GlyphAtlas")
.field("size", &self.size)
.field("glyphs", &self.entries.len())
.field("shelves", &self.shelves.len())
.field("stats", &self.stats)
.finish()
}
}