use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use xxhash_rust::xxh3::Xxh3;
use super::pbf::{decode_glyph_range, GlyphPbfError};
pub const SDF_EM_PX: f32 = 24.0;
pub const SDF_RADIUS_PX: f32 = 8.0;
pub const SDF_EDGE: f32 = 0.75;
pub const SDF_BORDER: u32 = 3;
pub const SDF_Y_OFFSET_PX: f32 = -17.0;
#[derive(Debug, Clone)]
pub struct SdfGlyph {
pub id: u32,
pub bitmap: Vec<u8>,
pub width: u32,
pub height: u32,
pub left: i32,
pub top: i32,
pub advance: u32,
}
pub type RangeFetcher = Box<dyn Fn(u32, u32) -> Result<Vec<u8>, String> + Send + Sync>;
enum RangeSlot {
Loaded {
glyphs: HashMap<u16, Arc<SdfGlyph>>,
hash: u64,
bytes: usize,
last_used: AtomicU64,
},
Failed,
}
impl RangeSlot {
fn empty() -> Self {
RangeSlot::Loaded {
glyphs: HashMap::new(),
hash: hash_glyphs(&HashMap::new()),
bytes: 0,
last_used: AtomicU64::new(0),
}
}
}
fn load_block(
ranges: &mut HashMap<u16, RangeSlot>,
block: u16,
) -> &mut HashMap<u16, Arc<SdfGlyph>> {
let slot = ranges.entry(block).or_insert_with(RangeSlot::empty);
if matches!(slot, RangeSlot::Failed) {
*slot = RangeSlot::empty();
}
match slot {
RangeSlot::Loaded { glyphs, .. } => glyphs,
RangeSlot::Failed => unreachable!("a failed slot was just replaced"),
}
}
fn hash_glyphs(glyphs: &HashMap<u16, Arc<SdfGlyph>>) -> u64 {
let mut ids: Vec<u16> = glyphs.keys().copied().collect();
ids.sort_unstable();
let mut h = Xxh3::new();
for id in ids {
let g = &glyphs[&id];
h.update(&id.to_le_bytes());
h.update(&g.width.to_le_bytes());
h.update(&g.height.to_le_bytes());
h.update(&g.left.to_le_bytes());
h.update(&g.top.to_le_bytes());
h.update(&g.advance.to_le_bytes());
h.update(&g.bitmap);
}
h.digest()
}
pub struct SdfFontStack {
ranges: RwLock<HashMap<u16, RangeSlot>>,
fetcher: Option<RangeFetcher>,
byte_budget: AtomicUsize,
clock: AtomicU64,
}
impl SdfFontStack {
pub fn new() -> Self {
SdfFontStack {
ranges: RwLock::new(HashMap::new()),
fetcher: None,
byte_budget: AtomicUsize::new(usize::MAX),
clock: AtomicU64::new(0),
}
}
pub fn with_fetcher(fetcher: RangeFetcher) -> Self {
SdfFontStack {
ranges: RwLock::new(HashMap::new()),
fetcher: Some(fetcher),
byte_budget: AtomicUsize::new(usize::MAX),
clock: AtomicU64::new(0),
}
}
pub fn set_byte_budget(&self, bytes: usize) {
self.byte_budget.store(bytes, Ordering::Relaxed);
}
pub fn byte_budget(&self) -> usize {
self.byte_budget.load(Ordering::Relaxed)
}
pub fn trim_to_budget(&self) -> usize {
let budget = self.byte_budget();
if budget == usize::MAX || self.loaded_size().1 <= budget {
return 0;
}
let mut ranges = self.ranges.write().expect("range map poisoned");
let mut live: Vec<(u64, u16, usize)> = ranges
.iter()
.filter_map(|(&block, slot)| match slot {
RangeSlot::Loaded {
bytes, last_used, ..
} => Some((last_used.load(Ordering::Relaxed), block, *bytes)),
RangeSlot::Failed => None,
})
.collect();
let mut total: usize = live.iter().map(|&(_, _, bytes)| bytes).sum();
live.sort_unstable();
let mut dropped = 0;
for (_, block, bytes) in live {
if total <= budget {
break;
}
ranges.remove(&block);
total = total.saturating_sub(bytes);
dropped += 1;
}
dropped
}
fn touch(&self, slot: &RangeSlot) {
if let RangeSlot::Loaded { last_used, .. } = slot {
last_used.store(
self.clock.fetch_add(1, Ordering::Relaxed),
Ordering::Relaxed,
);
}
}
pub fn has_fetcher(&self) -> bool {
self.fetcher.is_some()
}
pub fn block_of(c: char) -> Option<u16> {
u16::try_from(c as u32).ok().map(|u| u >> 8)
}
pub fn block_bounds(block: u16) -> (u32, u32) {
let start = u32::from(block) << 8;
(start, start + 255)
}
pub fn blocks_for(text: &str) -> Vec<u16> {
let mut blocks: Vec<u16> = text.chars().filter_map(Self::block_of).collect();
blocks.sort_unstable();
blocks.dedup();
blocks
}
pub fn is_loaded(&self, block: u16) -> bool {
self.ranges
.read()
.expect("range map poisoned")
.contains_key(&block)
}
pub fn loaded_size(&self) -> (usize, usize) {
let ranges = self.ranges.read().expect("range map poisoned");
let bytes = ranges
.values()
.map(|slot| match slot {
RangeSlot::Loaded { bytes, .. } => *bytes,
RangeSlot::Failed => 0,
})
.sum();
(ranges.len(), bytes)
}
pub fn insert_range(&self, bytes: &[u8]) -> Result<(), GlyphPbfError> {
let decoded = decode_glyph_range(bytes)?;
let mut ranges = self.ranges.write().expect("range map poisoned");
let mut touched: Vec<u16> = Vec::new();
if decoded.start >> 8 == decoded.end >> 8 {
let block = (decoded.start >> 8) as u16;
touched.push(block);
load_block(&mut ranges, block);
}
for g in decoded.glyphs {
let Ok(id) = u16::try_from(g.id) else {
continue;
};
touched.push(id >> 8);
load_block(&mut ranges, id >> 8).insert(id, Arc::new(g));
}
touched.sort_unstable();
touched.dedup();
let now = self.clock.fetch_add(1, Ordering::Relaxed);
for block in touched {
if let Some(RangeSlot::Loaded {
glyphs,
hash,
bytes,
last_used,
}) = ranges.get_mut(&block)
{
*hash = hash_glyphs(glyphs);
*bytes = glyphs.values().map(|g| g.bitmap.len()).sum();
last_used.store(now, Ordering::Relaxed);
}
}
Ok(())
}
pub fn glyph(&self, c: char) -> Option<Arc<SdfGlyph>> {
let block = Self::block_of(c)?;
self.ensure(block);
match self.ranges.read().expect("range map poisoned").get(&block) {
Some(slot @ RangeSlot::Loaded { glyphs, .. }) => {
self.touch(slot);
glyphs.get(&(c as u16)).cloned()
}
_ => None,
}
}
pub fn coverage(&self, c: char) -> SdfCoverage {
let Some(block) = Self::block_of(c) else {
return SdfCoverage::Absent;
};
self.ensure(block);
match self.ranges.read().expect("range map poisoned").get(&block) {
Some(slot @ RangeSlot::Loaded { glyphs, .. }) => {
self.touch(slot);
if glyphs.contains_key(&(c as u16)) {
SdfCoverage::Present
} else {
SdfCoverage::Absent
}
}
_ => SdfCoverage::RangeUnavailable,
}
}
pub fn ranges_hash(&self) -> u128 {
let map = self.ranges.read().expect("range map poisoned");
let mut entries: Vec<(u16, u64)> = map
.iter()
.map(|(&block, slot)| match slot {
RangeSlot::Loaded { hash, .. } => (block, *hash),
RangeSlot::Failed => (block, u64::MAX),
})
.collect();
entries.sort_unstable_by_key(|&(block, _)| block);
let mut h = Xxh3::new();
for (block, hash) in entries {
h.update(&block.to_le_bytes());
h.update(&hash.to_le_bytes());
}
h.digest128()
}
fn ensure(&self, block: u16) {
let Some(fetcher) = &self.fetcher else {
return;
};
if self.is_loaded(block) {
return;
}
let (start, end) = Self::block_bounds(block);
let slot = match fetcher(start, end) {
Ok(bytes) => match decode_glyph_range(&bytes) {
Ok(_) => {
let _ = self.insert_range(&bytes);
return;
}
Err(e) => {
tracing::warn!("glyph range {start}-{end}: decode failed: {e}");
RangeSlot::Failed
}
},
Err(e) => {
tracing::warn!("glyph range {start}-{end}: fetch failed: {e}");
RangeSlot::Failed
}
};
self.ranges
.write()
.expect("range map poisoned")
.insert(block, slot);
}
}
impl Default for SdfFontStack {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for SdfFontStack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let map = self.ranges.read().expect("range map poisoned");
f.debug_struct("SdfFontStack")
.field("ranges", &map.len())
.field("fetcher", &self.fetcher.is_some())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SdfCoverage {
Present,
Absent,
RangeUnavailable,
}