use astrelis_core::alloc::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShapeKey {
pub font_id: u32,
pub font_size_px: u16,
pub text_hash: u32,
pub wrap_width_bucket: u16,
}
impl ShapeKey {
pub fn new(font_id: u32, font_size: f32, text_content: &str, wrap_width: Option<f32>) -> Self {
Self {
font_id,
font_size_px: font_size.round() as u16,
text_hash: fxhash::hash32(text_content),
wrap_width_bucket: wrap_width.map(Self::bucket_width).unwrap_or(0),
}
}
fn bucket_width(width: f32) -> u16 {
(width / 4.0).round() as u16
}
pub fn bucketed_width(&self) -> Option<f32> {
if self.wrap_width_bucket == 0 {
None
} else {
Some(self.wrap_width_bucket as f32 * 4.0)
}
}
}
#[derive(Debug, Clone)]
pub struct ShapedTextData {
pub content: String,
pub bounds: (f32, f32),
pub shaped_at_version: u32,
pub render_count: u64,
}
impl ShapedTextData {
pub fn new(content: String, bounds: (f32, f32), version: u32) -> Self {
Self {
content,
bounds,
shaped_at_version: version,
render_count: 0,
}
}
}
pub struct TextShapeCache {
cache: HashMap<ShapeKey, Arc<ShapedTextData>>,
pub hits: u64,
pub misses: u64,
}
impl TextShapeCache {
pub fn new() -> Self {
Self {
cache: HashMap::with_capacity(256),
hits: 0,
misses: 0,
}
}
pub fn get_or_shape<F>(&mut self, key: ShapeKey, shape_fn: F) -> Arc<ShapedTextData>
where
F: FnOnce() -> ShapedTextData,
{
if let Some(cached) = self.cache.get_mut(&key) {
self.hits += 1;
Arc::get_mut(cached).map(|data| data.render_count += 1);
return cached.clone();
}
self.misses += 1;
let shaped = Arc::new(shape_fn());
self.cache.insert(key, shaped.clone());
shaped
}
pub fn get(&mut self, key: &ShapeKey) -> Option<Arc<ShapedTextData>> {
let result = self.cache.get_mut(key).map(|cached| {
Arc::get_mut(cached).map(|data| data.render_count += 1);
cached.clone()
});
if result.is_some() {
self.hits += 1;
} else {
self.misses += 1;
}
result
}
pub fn insert(&mut self, key: ShapeKey, data: ShapedTextData) -> Arc<ShapedTextData> {
let arc_data = Arc::new(data);
self.cache.insert(key, arc_data.clone());
arc_data
}
pub fn clear(&mut self) {
self.cache.clear();
self.hits = 0;
self.misses = 0;
}
pub fn prune_old_versions(&mut self, min_version: u32) {
self.cache
.retain(|_key, data| data.shaped_at_version >= min_version);
}
pub fn hit_rate(&self) -> f32 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f32 / total as f32
}
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn stats_string(&self) -> String {
let total_renders: u64 = self
.cache
.values()
.filter_map(|arc| Some(arc.render_count))
.sum();
format!(
"TextCache: {} entries, {:.1}% hit rate ({} hits, {} misses), {} total renders",
self.len(),
self.hit_rate() * 100.0,
self.hits,
self.misses,
total_renders
)
}
pub fn avg_renders_per_entry(&self) -> f32 {
if self.cache.is_empty() {
return 0.0;
}
let total_renders: u64 = self
.cache
.values()
.filter_map(|arc| Some(arc.render_count))
.sum();
total_renders as f32 / self.cache.len() as f32
}
}
impl Default for TextShapeCache {
fn default() -> Self {
Self::new()
}
}