#![allow(dead_code)]
use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::mem::size_of;
use std::rc::Weak;
use slate_renderer::RendererObserver;
use slate_text::types::{FontId, ShapedGlyph, ShapedLine};
use crate::types::{Bounds, ElementId};
const DEFAULT_MEMORY_CAP: usize = 50 * 1024 * 1024;
const PER_ELEMENT_SIZE_CAP: usize = 64 * 1024;
pub struct TextShapingCache {
entries: HashMap<ElementId, CachedTextShape>,
current_frame: u64,
memory_used: usize,
memory_cap: usize,
hit_count: u64,
miss_count: u64,
}
pub struct CachedTextShape {
pub input_hash: u64,
pub lines: Vec<ShapedLine>,
pub font_id: FontId,
pub size_px: f32,
pub last_seen_frame: u64,
pub size_bytes: usize,
}
impl CachedTextShape {
fn empty(frame: u64) -> Self {
Self {
input_hash: 0,
lines: Vec::new(),
font_id: FontId::default(),
size_px: 0.0,
last_seen_frame: frame,
size_bytes: 0,
}
}
#[allow(clippy::manual_slice_size_calculation)] fn compute_size(lines: &[ShapedLine]) -> usize {
let glyph_count: usize = lines.iter().map(|l| l.glyphs.len()).sum();
glyph_count * size_of::<ShapedGlyph>() + lines.len() * size_of::<ShapedLine>()
}
}
impl TextShapingCache {
pub fn new() -> Self {
Self::with_memory_cap(DEFAULT_MEMORY_CAP)
}
pub fn with_memory_cap(memory_cap: usize) -> Self {
Self {
entries: HashMap::new(),
current_frame: 0,
memory_used: 0,
memory_cap,
hit_count: 0,
miss_count: 0,
}
}
pub fn get(&mut self, id: ElementId, input_hash: u64) -> Option<&[ShapedLine]> {
if input_hash == 0 {
return None; }
if let Some(entry) = self.entries.get_mut(&id)
&& entry.input_hash == input_hash
{
entry.last_seen_frame = self.current_frame;
self.hit_count += 1;
return Some(&entry.lines);
}
None
}
pub fn insert(
&mut self,
id: ElementId,
input_hash: u64,
lines: Vec<ShapedLine>,
font_id: FontId,
size_px: f32,
) {
if input_hash == 0 {
return; }
let new_size = CachedTextShape::compute_size(&lines);
if new_size > PER_ELEMENT_SIZE_CAP {
self.miss_count += 1;
return;
}
if let Some(old_entry) = self.entries.get(&id) {
self.memory_used = self.memory_used.saturating_sub(old_entry.size_bytes);
}
self.memory_used += new_size;
let entry = CachedTextShape {
input_hash,
lines,
font_id,
size_px,
last_seen_frame: self.current_frame,
size_bytes: new_size,
};
self.entries.insert(id, entry);
self.miss_count += 1;
}
pub fn with_shape<S, R>(
&mut self,
id: ElementId,
input_hash: u64,
font_id: FontId,
size_px: f32,
shape_fresh: S,
on_hit: impl FnOnce(&[ShapedLine]) -> R,
) -> R
where
S: FnOnce() -> Vec<ShapedLine>,
{
if input_hash == 0 {
let lines = shape_fresh();
return on_hit(&lines);
}
if let Some(entry) = self.entries.get_mut(&id)
&& entry.input_hash == input_hash
{
entry.last_seen_frame = self.current_frame;
self.hit_count += 1;
return on_hit(&entry.lines);
}
let lines = shape_fresh();
let new_size = CachedTextShape::compute_size(&lines);
let result = on_hit(&lines);
if new_size <= PER_ELEMENT_SIZE_CAP {
if let Some(old_entry) = self.entries.get(&id) {
self.memory_used = self.memory_used.saturating_sub(old_entry.size_bytes);
}
self.memory_used += new_size;
let entry = CachedTextShape {
input_hash,
lines,
font_id,
size_px,
last_seen_frame: self.current_frame,
size_bytes: new_size,
};
self.entries.insert(id, entry);
}
self.miss_count += 1;
result
}
pub fn advance_frame(&mut self) {
self.current_frame = self.current_frame.wrapping_add(1);
}
pub fn gc(&mut self) {
let cutoff = self.current_frame.saturating_sub(2);
self.entries.retain(|_, entry| {
if entry.last_seen_frame >= cutoff {
true
} else {
self.memory_used = self.memory_used.saturating_sub(entry.size_bytes);
false
}
});
if self.memory_used > self.memory_cap {
self.lru_evict_quarter();
}
}
fn lru_evict_quarter(&mut self) {
if self.entries.is_empty() {
return;
}
let mut ids_by_age: Vec<_> = self
.entries
.iter()
.map(|(id, e)| (*id, e.last_seen_frame, e.size_bytes))
.collect();
ids_by_age.sort_by_key(|(_, frame, _)| *frame);
let evict_count = ids_by_age.len().div_ceil(4);
for (id, _, size_bytes) in ids_by_age.into_iter().take(evict_count) {
self.entries.remove(&id);
self.memory_used = self.memory_used.saturating_sub(size_bytes);
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.memory_used = 0;
}
pub fn current_frame(&self) -> u64 {
self.current_frame
}
pub fn memory_used(&self) -> usize {
self.memory_used
}
pub fn hit_count(&self) -> u64 {
self.hit_count
}
pub fn miss_count(&self) -> u64 {
self.miss_count
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for TextShapingCache {
fn default() -> Self {
Self::new()
}
}
#[inline]
pub fn hash_f32<H: Hasher>(v: f32, h: &mut H) {
let q = if v.is_nan() {
0
} else {
(v * 256.0).round().clamp(i32::MIN as f32, i32::MAX as f32) as i32
};
q.hash(h);
}
#[inline]
pub fn hash_bounds<H: Hasher>(bounds: &Bounds, h: &mut H) {
hash_f32(bounds.origin.x, h);
hash_f32(bounds.origin.y, h);
hash_f32(bounds.size.width, h);
hash_f32(bounds.size.height, h);
}
#[inline]
pub fn hash_color<H: Hasher>(color: &[f32; 4], h: &mut H) {
for &c in color {
hash_f32(c, h);
}
}
pub struct TextShapingCacheObserver {
inner: Weak<RefCell<TextShapingCache>>,
}
impl TextShapingCacheObserver {
pub fn new(inner: Weak<RefCell<TextShapingCache>>) -> Self {
Self { inner }
}
}
impl RendererObserver for TextShapingCacheObserver {
fn on_renderer_recreated(&self, generation: u64) {
log::debug!(target: "slate::device_lost",
"TextShapingCacheObserver: clearing cache (gen={})", generation);
if let Some(strong) = self.inner.upgrade() {
strong.borrow_mut().clear();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use slate_text::types::ShapedLine;
fn make_shaped_line(glyph_count: usize) -> ShapedLine {
ShapedLine {
glyphs: (0..glyph_count)
.map(|i| ShapedGlyph {
glyph_id: i as u32,
font_id: FontId::PRIMARY,
font_handle: Default::default(),
x_advance_lpx: 10.0,
position_lpx: [0.0, 0.0],
cluster: 0,
direction: slate_text::Direction::Ltr,
})
.collect(),
width_lpx: glyph_count as f32 * 10.0,
ascent_lpx: 14.0,
descent_lpx: -4.0,
y_offset_lpx: 0.0,
base_direction: slate_text::Direction::Ltr,
runs: Vec::new(),
}
}
#[test]
fn cache_hit_miss() {
let mut cache = TextShapingCache::new();
let id = ElementId::from_raw(1);
let hash = 12345;
assert!(cache.get(id, hash).is_none());
cache.insert(id, hash, vec![make_shaped_line(5)], FontId::PRIMARY, 14.0);
let lines = cache.get(id, hash);
assert!(lines.is_some());
assert_eq!(lines.unwrap().len(), 1);
assert_eq!(lines.unwrap()[0].glyphs.len(), 5);
assert!(cache.get(id, 99999).is_none());
}
#[test]
fn cache_gc() {
let mut cache = TextShapingCache::new();
let id = ElementId::from_raw(1);
let hash = 12345;
cache.insert(id, hash, vec![make_shaped_line(5)], FontId::PRIMARY, 14.0);
assert_eq!(cache.len(), 1);
cache.advance_frame();
cache.gc();
assert_eq!(cache.len(), 1);
cache.advance_frame();
cache.gc();
assert_eq!(cache.len(), 1);
cache.advance_frame();
cache.gc();
assert_eq!(cache.len(), 0);
}
#[test]
fn sentinel_hash_zero_bypasses_cache() {
let mut cache = TextShapingCache::new();
let id = ElementId::from_raw(1);
cache.insert(id, 0, vec![make_shaped_line(5)], FontId::PRIMARY, 14.0);
assert!(cache.get(id, 0).is_none());
assert_eq!(cache.len(), 0);
}
#[test]
fn with_shape_callback() {
let mut cache = TextShapingCache::new();
let id = ElementId::from_raw(1);
let hash = 12345;
let mut shape_called = false;
let result = cache.with_shape(
id,
hash,
FontId::PRIMARY,
14.0,
|| {
shape_called = true;
vec![make_shaped_line(5)]
},
|lines| lines.len(),
);
assert!(shape_called);
assert_eq!(result, 1);
shape_called = false;
let result = cache.with_shape(
id,
hash,
FontId::PRIMARY,
14.0,
|| {
shape_called = true;
vec![make_shaped_line(5)]
},
|lines| lines.len(),
);
assert!(!shape_called);
assert_eq!(result, 1);
}
#[test]
fn hash_f32_nan_safe() {
use std::collections::hash_map::DefaultHasher;
let mut h1 = DefaultHasher::new();
let mut h2 = DefaultHasher::new();
hash_f32(f32::NAN, &mut h1);
hash_f32(f32::NAN, &mut h2);
assert_eq!(h1.finish(), h2.finish());
}
#[test]
fn hash_f32_quantization() {
use std::collections::hash_map::DefaultHasher;
let mut h1 = DefaultHasher::new();
let mut h2 = DefaultHasher::new();
hash_f32(100.0, &mut h1);
hash_f32(100.001, &mut h2);
assert_eq!(h1.finish(), h2.finish());
let mut h3 = DefaultHasher::new();
let mut h4 = DefaultHasher::new();
hash_f32(100.0, &mut h3);
hash_f32(100.01, &mut h4);
assert_ne!(h3.finish(), h4.finish());
}
#[test]
fn memory_cap_lru_eviction() {
let mut cache = TextShapingCache::with_memory_cap(1000);
let font_id = FontId::PRIMARY;
for i in 0..10 {
let id = ElementId::from_raw(i);
cache.insert(id, i + 1, vec![make_shaped_line(10)], font_id, 14.0);
cache.advance_frame(); }
cache.gc();
assert!(cache.len() < 10);
assert!(cache.memory_used() <= 1000 || cache.len() <= 7); }
}