use std::collections::HashMap;
use slate_renderer::atlas::Atlas;
use slate_renderer::glyph_pipeline::allocate_glyph;
use crate::backend::{Font, TextBackend};
use crate::error::TextError;
use crate::font_handle::FontHandle;
use crate::types::{CachedGlyph, GlyphBitmap, GlyphMetrics};
const DEFAULT_MAX_ENTRIES: usize = 8192;
pub struct GlyphCache {
cache: HashMap<(FontHandle, u32, u8), CachedGlyph>,
max_entries: usize,
}
impl GlyphCache {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
max_entries: DEFAULT_MAX_ENTRIES,
}
}
pub fn with_max_entries(max_entries: usize) -> Self {
Self {
cache: HashMap::new(),
max_entries,
}
}
pub fn get(&self, font: FontHandle, glyph_id: u32, variant: u8) -> Option<&CachedGlyph> {
self.cache.get(&(font, glyph_id, variant))
}
fn live_hit(&self, key: &(FontHandle, u32, u8), atlas: &Atlas) -> bool {
self.cache
.get(key)
.is_some_and(|cg| atlas.is_live(cg.alloc.alloc_id, cg.alloc.token))
}
pub fn materialize<B: TextBackend>(
&mut self,
backend: &B,
font: &B::Font,
glyph_id: u32,
variant: u8,
atlas: &mut Atlas,
queue: &wgpu::Queue,
) -> Result<bool, TextError> {
let key = (font.handle(), glyph_id, variant);
if self.live_hit(&key, atlas) {
return Ok(false);
}
if self.cache.len() >= self.max_entries {
log::warn!(
"GlyphCache at capacity ({} entries); clearing cache to free memory",
self.max_entries
);
for (_, cg) in self.cache.drain() {
atlas.deallocate(cg.alloc.alloc_id);
}
}
let bitmap = backend.rasterize_glyph(font, glyph_id, variant)?;
if bitmap.width == 0 || bitmap.height == 0 {
return Ok(false);
}
let alloc = allocate_glyph(atlas, bitmap.width, bitmap.height)
.map_err(|e| TextError::RasterizationFailed(format!("atlas alloc: {e:?}")))?;
let padded = pad_with_gutter(&bitmap.alpha, bitmap.width, bitmap.height);
atlas.upload(queue, alloc.alloc_id, &padded);
let metrics = GlyphMetrics::from_bitmap(&bitmap);
self.cache.insert(key, CachedGlyph { alloc, metrics });
Ok(true)
}
pub fn materialize_by_handle<B: TextBackend>(
&mut self,
backend: &B,
handle: FontHandle,
glyph_id: u32,
variant: u8,
atlas: &mut Atlas,
queue: &wgpu::Queue,
) -> Result<bool, TextError> {
let Some(font) = backend.font_for(handle) else {
return Ok(false);
};
let key = (handle, glyph_id, variant);
if self.live_hit(&key, atlas) {
return Ok(false);
}
if self.cache.len() >= self.max_entries {
log::warn!(
"GlyphCache at capacity ({} entries); clearing cache to free memory",
self.max_entries
);
for (_, cg) in self.cache.drain() {
atlas.deallocate(cg.alloc.alloc_id);
}
}
let bitmap = backend.rasterize_glyph(font, glyph_id, variant)?;
if bitmap.width == 0 || bitmap.height == 0 {
return Ok(false);
}
let alloc = allocate_glyph(atlas, bitmap.width, bitmap.height)
.map_err(|e| TextError::RasterizationFailed(format!("atlas alloc: {e:?}")))?;
let padded = pad_with_gutter(&bitmap.alpha, bitmap.width, bitmap.height);
atlas.upload(queue, alloc.alloc_id, &padded);
let metrics = GlyphMetrics::from_bitmap(&bitmap);
self.cache.insert(key, CachedGlyph { alloc, metrics });
Ok(true)
}
pub fn touch(&mut self, atlas: &mut Atlas, font: FontHandle, glyph_id: u32, variant: u8) {
if let Some(cg) = self.cache.get(&(font, glyph_id, variant)) {
atlas.touch(cg.alloc.alloc_id);
}
}
pub fn clear(&mut self, atlas: &mut Atlas) {
for (_, cg) in self.cache.drain() {
atlas.deallocate(cg.alloc.alloc_id);
}
}
pub fn clear_cpu_state(&mut self) {
self.cache.clear();
}
pub fn cache_len(&self) -> usize {
self.cache.len()
}
pub fn max_entries(&self) -> usize {
self.max_entries
}
}
impl Default for GlyphCache {
fn default() -> Self {
Self::new()
}
}
pub fn pad_with_gutter(src: &[u8], w: u32, h: u32) -> Vec<u8> {
let pw = (w + 2) as usize;
let ph = (h + 2) as usize;
let mut buf = vec![0u8; pw * ph];
for y in 0..h as usize {
let src_off = y * w as usize;
let dst_off = (y + 1) * pw + 1;
buf[dst_off..dst_off + w as usize].copy_from_slice(&src[src_off..src_off + w as usize]);
}
buf
}
impl GlyphMetrics {
pub fn from_bitmap(bitmap: &GlyphBitmap) -> Self {
Self {
width: bitmap.width,
height: bitmap.height,
bearing_x_lpx: bitmap.bearing_x_lpx,
bearing_y_lpx: bitmap.bearing_y_lpx,
advance_x_lpx: bitmap.advance_x_lpx,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_max_entries_is_set() {
let cache = GlyphCache::new();
assert_eq!(cache.max_entries(), DEFAULT_MAX_ENTRIES);
assert_eq!(cache.cache_len(), 0);
}
#[test]
fn with_max_entries_configures_limit() {
let cache = GlyphCache::with_max_entries(64);
assert_eq!(cache.max_entries(), 64);
}
#[test]
fn pad_with_gutter_dimensions() {
let src = vec![1u8, 2, 3, 4];
let result = pad_with_gutter(&src, 2, 2);
assert_eq!(result.len(), 4 * 4);
assert_eq!(&result[0..4], &[0u8, 0, 0, 0]);
assert_eq!(&result[4..8], &[0u8, 1, 2, 0]);
assert_eq!(&result[8..12], &[0u8, 3, 4, 0]);
assert_eq!(&result[12..16], &[0u8, 0, 0, 0]);
}
}