use lru::LruCache;
use std::num::NonZeroUsize;
use std::sync::Arc;
use tracing::debug;
const MAX_TEXT_RUN_CACHE_SIZE: usize = 256;
#[derive(Clone, Debug)]
pub struct CachedTextRun {
pub glyphs: Arc<Vec<ShapedGlyph>>,
pub font_id: usize,
pub has_emoji: bool,
pub advance_width: f32,
pub shaping_features: Option<Arc<Vec<u8>>>,
pub vertices: Option<Arc<Vec<u8>>>,
pub base_position: Option<(f32, f32)>,
pub cached_color: Option<[f32; 4]>,
pub font_size: f32,
}
#[derive(Clone, Debug)]
pub struct ShapedGlyph {
pub glyph_id: u32,
pub x_advance: f32,
pub y_advance: f32,
pub x_offset: f32,
pub y_offset: f32,
pub cluster: u32,
pub atlas_coords: Option<(f32, f32, f32, f32)>, pub atlas_layer: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TextRunKey {
pub text: String,
pub font_id: usize,
pub font_size_scaled: u32,
pub color: Option<[u32; 4]>, }
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FontAttributes {
pub weight: u16,
pub style: u8, pub stretch: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum TextDirection {
LeftToRight,
RightToLeft,
}
pub struct TextRunCache {
cache_with_color: LruCache<TextRunKey, CachedTextRun>,
cache_without_color: LruCache<TextRunKey, CachedTextRun>,
}
impl TextRunCache {
pub fn new() -> Self {
Self {
cache_with_color: LruCache::new(
NonZeroUsize::new(MAX_TEXT_RUN_CACHE_SIZE * 2).unwrap(),
),
cache_without_color: LruCache::new(
NonZeroUsize::new(MAX_TEXT_RUN_CACHE_SIZE).unwrap(),
),
}
}
pub fn get(&mut self, key: &TextRunKey) -> Option<CacheHitType<'_>> {
if let Some(cached_run) = self.cache_with_color.get(key) {
if cached_run.vertices.is_some()
&& cached_run.cached_color.is_some()
&& key.color.is_some()
{
return Some(CacheHitType::FullRender(cached_run));
} else if cached_run.shaping_features.is_some() {
return Some(CacheHitType::ShapingOnly(cached_run));
} else {
return Some(CacheHitType::GlyphsOnly(cached_run));
}
}
if key.color.is_some() {
let mut key_without_color = key.clone();
key_without_color.color = None;
if let Some(cached_run) = self.cache_without_color.get(&key_without_color) {
return Some(CacheHitType::ShapingOnly(cached_run));
}
}
None
}
pub fn insert(&mut self, key: TextRunKey, run: CachedTextRun) {
if key.color.is_some() {
self.cache_with_color.put(key.clone(), run.clone());
}
let mut key_without_color = key;
key_without_color.color = None;
self.cache_without_color.put(key_without_color, run);
}
pub fn update_vertices(
&mut self,
key: &TextRunKey,
vertices: Vec<u8>,
base_position: (f32, f32),
color: [f32; 4],
) -> bool {
if let Some(cached_run) = self.cache_with_color.get_mut(key) {
cached_run.vertices = Some(Arc::new(vertices));
cached_run.base_position = Some(base_position);
cached_run.cached_color = Some(color);
return true;
}
false
}
pub fn clear(&mut self) {
self.cache_with_color.clear();
self.cache_without_color.clear();
debug!("UnifiedTextRunCache cleared due to font change");
}
pub fn is_full(&self) -> bool {
self.cache_with_color.len() >= self.cache_with_color.cap().get()
}
pub fn utilization(&self) -> f64 {
self.cache_with_color.len() as f64 / self.cache_with_color.cap().get() as f64
}
pub fn resize(&mut self, new_capacity: usize) {
let new_cap = NonZeroUsize::new(new_capacity).unwrap();
self.cache_with_color.resize(new_cap);
let shaping_cap = NonZeroUsize::new(new_capacity / 2).unwrap();
self.cache_without_color.resize(shaping_cap);
}
pub fn capacity(&self) -> usize {
self.cache_with_color.cap().get()
}
pub fn len(&self) -> usize {
self.cache_with_color.len()
}
pub fn is_empty(&self) -> bool {
self.cache_with_color.is_empty()
}
pub fn peek(&self, key: &TextRunKey) -> Option<CacheHitType<'_>> {
if let Some(cached_run) = self.cache_with_color.peek(key) {
if cached_run.vertices.is_some()
&& cached_run.cached_color.is_some()
&& key.color.is_some()
{
return Some(CacheHitType::FullRender(cached_run));
} else if cached_run.shaping_features.is_some() {
return Some(CacheHitType::ShapingOnly(cached_run));
} else {
return Some(CacheHitType::GlyphsOnly(cached_run));
}
}
if key.color.is_some() {
let mut key_without_color = key.clone();
key_without_color.color = None;
if let Some(cached_run) = self.cache_without_color.peek(&key_without_color) {
return Some(CacheHitType::ShapingOnly(cached_run));
}
}
None
}
}
#[derive(Debug)]
pub enum CacheHitType<'a> {
FullRender(&'a CachedTextRun),
ShapingOnly(&'a CachedTextRun),
GlyphsOnly(&'a CachedTextRun),
}
impl Default for TextRunCache {
fn default() -> Self {
Self::new()
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_text_run_key(
text: &str,
font_id: usize,
font_size: f32,
color: Option<[f32; 4]>,
) -> TextRunKey {
TextRunKey {
text: text.to_string(),
font_id,
font_size_scaled: (font_size * 100.0) as u32,
color: color.map(|c| {
[
(c[0] * 1000.0) as u32,
(c[1] * 1000.0) as u32,
(c[2] * 1000.0) as u32,
(c[3] * 1000.0) as u32,
]
}),
}
}
pub fn create_shaping_key(text: &str, font_id: usize, font_size: f32) -> TextRunKey {
create_text_run_key(text, font_id, font_size, None)
}
#[allow(clippy::too_many_arguments)]
pub fn create_cached_text_run(
glyphs: Vec<ShapedGlyph>,
font_id: usize,
font_size: f32,
has_emoji: bool,
shaping_features: Option<Vec<u8>>,
vertices: Option<Vec<u8>>,
base_position: Option<(f32, f32)>,
color: Option<[f32; 4]>,
) -> CachedTextRun {
let advance_width = glyphs.iter().map(|g| g.x_advance).sum();
CachedTextRun {
glyphs: Arc::new(glyphs),
font_id,
has_emoji,
advance_width,
shaping_features: shaping_features.map(Arc::new),
vertices: vertices.map(Arc::new),
base_position,
cached_color: color,
font_size,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unified_text_run_cache_basic() {
let mut cache = TextRunCache::new();
let key = create_text_run_key("hello world", 0, 12.0, Some([1.0, 1.0, 1.0, 1.0]));
let run = create_cached_text_run(
vec![],
0,
12.0,
false,
None,
None,
None,
Some([1.0, 1.0, 1.0, 1.0]),
);
assert!(cache.get(&key).is_none());
cache.insert(key.clone(), run.clone());
assert!(cache.get(&key).is_some());
assert_eq!(cache.len(), 1);
}
#[test]
fn test_shaping_cache_fallback() {
let mut cache = TextRunCache::new();
let shaping_key = create_shaping_key("hello", 0, 12.0);
let run = create_cached_text_run(
vec![],
0,
12.0,
false,
Some(vec![1, 2, 3]), None,
None,
None,
);
cache.insert(shaping_key, run);
let render_key =
create_text_run_key("hello", 0, 12.0, Some([1.0, 0.0, 0.0, 1.0]));
if let Some(hit_type) = cache.get(&render_key) {
match hit_type {
CacheHitType::ShapingOnly(_) => {
}
CacheHitType::GlyphsOnly(_) => {
}
_ => panic!("Expected shaping-only or glyphs-only cache hit"),
}
} else {
panic!("Expected cache hit");
}
}
#[test]
fn test_vertex_cache_update() {
let mut cache = TextRunCache::new();
let key = create_text_run_key("test", 0, 12.0, Some([1.0, 1.0, 1.0, 1.0]));
let run = create_cached_text_run(vec![], 0, 12.0, false, None, None, None, None);
cache.insert(key.clone(), run);
let vertices = vec![];
let updated =
cache.update_vertices(&key, vertices, (10.0, 20.0), [1.0, 1.0, 1.0, 1.0]);
assert!(updated);
if let Some(hit_type) = cache.get(&key) {
match hit_type {
CacheHitType::FullRender(cached_run) => {
assert!(cached_run.vertices.is_some());
assert_eq!(cached_run.base_position, Some((10.0, 20.0)));
}
_ => panic!("Expected full render cache hit"),
}
} else {
panic!("Expected cache hit");
}
}
#[test]
fn test_lru_eviction() {
let mut cache = TextRunCache::new();
let capacity = cache.capacity();
for i in 0..capacity + 1 {
let key = create_text_run_key(
&format!("text{i}"),
0,
12.0,
Some([1.0, 1.0, 1.0, 1.0]),
);
let run =
create_cached_text_run(vec![], 0, 12.0, false, None, None, None, None);
cache.insert(key, run);
}
assert_eq!(cache.len(), capacity);
let first_key = create_text_run_key("text0", 0, 12.0, Some([1.0, 1.0, 1.0, 1.0]));
assert!(cache.get(&first_key).is_none());
let last_key = create_text_run_key(
&format!("text{capacity}"),
0,
12.0,
Some([1.0, 1.0, 1.0, 1.0]),
);
assert!(cache.get(&last_key).is_some());
}
#[test]
fn test_cache_resize() {
let mut cache = TextRunCache::new();
for i in 0..10 {
let key = create_text_run_key(
&format!("text{i}"),
0,
12.0,
Some([1.0, 1.0, 1.0, 1.0]),
);
let run =
create_cached_text_run(vec![], 0, 12.0, false, None, None, None, None);
cache.insert(key, run);
}
let new_capacity = 5;
cache.resize(new_capacity);
assert_eq!(cache.capacity(), new_capacity);
assert!(cache.len() <= new_capacity);
}
#[test]
fn test_peek_functionality() {
let mut cache = TextRunCache::new();
let key = create_text_run_key("peek_test", 0, 12.0, Some([1.0, 1.0, 1.0, 1.0]));
let run = create_cached_text_run(vec![], 0, 12.0, false, None, None, None, None);
cache.insert(key.clone(), run);
assert!(cache.peek(&key).is_some());
assert!(cache.get(&key).is_some());
}
#[test]
fn test_utilization() {
let mut cache = TextRunCache::new();
assert_eq!(cache.utilization(), 0.0);
for i in 0..5 {
let key = create_text_run_key(
&format!("util_test{i}"),
0,
12.0,
Some([1.0, 1.0, 1.0, 1.0]),
);
let run =
create_cached_text_run(vec![], 0, 12.0, false, None, None, None, None);
cache.insert(key, run);
}
let utilization = cache.utilization();
assert!(utilization > 0.0);
assert!(utilization <= 1.0);
}
#[test]
fn test_cache_empty_and_clear() {
let mut cache = TextRunCache::new();
assert!(cache.is_empty());
let key = create_text_run_key("test", 0, 12.0, Some([1.0, 1.0, 1.0, 1.0]));
let run = create_cached_text_run(vec![], 0, 12.0, false, None, None, None, None);
cache.insert(key, run);
assert!(!cache.is_empty());
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
}