use std::hash::Hasher;
use std::mem::MaybeUninit;
use std::sync::Arc;
use tracing::debug;
use wyhash::WyHash;
const NUM_BUCKETS: usize = 256;
const BUCKET_SIZE: usize = 8;
#[derive(Clone, Debug)]
pub struct CachedTextRun {
pub glyphs: Arc<Vec<ShapedGlyph>>,
pub font_id: usize,
pub has_emoji: bool,
pub advance_width: f32,
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 type TextRunKey = u64;
struct Entry {
key: u64,
value: CachedTextRun,
}
#[inline]
unsafe fn move_entries(items: *mut Entry, dst: usize, src: usize, count: usize) {
if count == 0 {
return;
}
std::ptr::copy(items.add(src), items.add(dst), count);
}
#[inline]
unsafe fn rotate_to_end(items: *mut Entry, pos: usize, len: usize) {
if pos >= len - 1 {
return;
}
let tmp = std::ptr::read(items.add(pos));
move_entries(items, pos, pos + 1, len - pos - 1);
std::ptr::write(items.add(len - 1), tmp);
}
#[inline]
unsafe fn rotate_in(items: *mut Entry, len: usize, new: Entry) -> Entry {
let removed = std::ptr::read(items);
move_entries(items, 0, 1, len - 1);
std::ptr::write(items.add(len - 1), new);
removed
}
pub struct TextRunCache {
buckets: Box<[[MaybeUninit<Entry>; BUCKET_SIZE]; NUM_BUCKETS]>,
lengths: [u8; NUM_BUCKETS],
}
impl TextRunCache {
pub fn new() -> Self {
Self {
buckets: unsafe {
Box::new(
MaybeUninit::<
[[MaybeUninit<Entry>; BUCKET_SIZE]; NUM_BUCKETS],
>::uninit()
.assume_init(),
)
},
lengths: [0; NUM_BUCKETS],
}
}
#[inline]
fn bucket_ptr_mut(&mut self, idx: usize) -> *mut Entry {
self.buckets[idx].as_mut_ptr() as *mut Entry
}
#[inline]
pub fn get(&mut self, key: &TextRunKey) -> Option<&CachedTextRun> {
let idx = (*key as usize) & (NUM_BUCKETS - 1);
let len = self.lengths[idx] as usize;
let ptr = self.bucket_ptr_mut(idx);
let mut i = len;
while i > 0 {
i -= 1;
if unsafe { (*ptr.add(i)).key == *key } {
unsafe { rotate_to_end(ptr, i, len) };
return Some(unsafe { &(*ptr.add(len - 1)).value });
}
}
None
}
#[inline]
pub fn insert(&mut self, key: TextRunKey, value: CachedTextRun) {
let idx = (key as usize) & (NUM_BUCKETS - 1);
let len = self.lengths[idx] as usize;
let ptr = self.bucket_ptr_mut(idx);
for i in 0..len {
if unsafe { (*ptr.add(i)).key == key } {
unsafe {
std::ptr::drop_in_place(&mut (*ptr.add(i)).value);
std::ptr::write(ptr.add(i), Entry { key, value });
rotate_to_end(ptr, i, len);
}
return;
}
}
if len < BUCKET_SIZE {
self.buckets[idx][len] = MaybeUninit::new(Entry { key, value });
self.lengths[idx] += 1;
return;
}
let evicted = unsafe { rotate_in(ptr, BUCKET_SIZE, Entry { key, value }) };
drop(evicted);
}
pub fn clear(&mut self) {
for i in 0..NUM_BUCKETS {
let len = self.lengths[i] as usize;
let ptr = self.bucket_ptr_mut(i);
for j in 0..len {
unsafe { std::ptr::drop_in_place(ptr.add(j)) };
}
self.lengths[i] = 0;
}
debug!("TextRunCache cleared");
}
}
impl Drop for TextRunCache {
fn drop(&mut self) {
for i in 0..NUM_BUCKETS {
let len = self.lengths[i] as usize;
let ptr = self.bucket_ptr_mut(i);
for j in 0..len {
unsafe { std::ptr::drop_in_place(ptr.add(j)) };
}
}
}
}
impl Default for TextRunCache {
fn default() -> Self {
Self::new()
}
}
pub fn create_text_run_key(text: &str, font_id: usize, font_size: f32) -> TextRunKey {
let mut hasher = WyHash::with_seed(0);
for (cluster, ch) in text.chars().enumerate() {
if ch == '\u{FE0E}' || ch == '\u{FE0F}' {
continue;
}
hasher.write_u32(ch as u32);
hasher.write_usize(cluster);
}
hasher.write_usize(text.chars().count());
hasher.write_usize(font_id);
hasher.write_u32((font_size * 100.0) as u32);
hasher.finish()
}
pub fn create_cached_text_run(
glyphs: Vec<ShapedGlyph>,
font_id: usize,
font_size: f32,
has_emoji: bool,
) -> CachedTextRun {
let advance_width = glyphs.iter().map(|g| g.x_advance).sum();
CachedTextRun {
glyphs: Arc::new(glyphs),
font_id,
has_emoji,
advance_width,
font_size,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_insert_and_get() {
let mut cache = TextRunCache::new();
let key = create_text_run_key("hello world", 0, 12.0);
let run = create_cached_text_run(vec![], 0, 12.0, false);
assert!(cache.get(&key).is_none());
cache.insert(key, run);
assert!(cache.get(&key).is_some());
}
#[test]
fn test_position_independence() {
let mut cache = TextRunCache::new();
let key1 = create_text_run_key("hello", 0, 12.0);
let key2 = create_text_run_key("hello", 0, 12.0);
assert_eq!(key1, key2);
cache.insert(key1, create_cached_text_run(vec![], 0, 12.0, false));
assert!(cache.get(&key2).is_some());
}
#[test]
fn test_hash_consistency() {
let key1 = create_text_run_key("test", 0, 12.0);
let key2 = create_text_run_key("test", 0, 12.0);
assert_eq!(key1, key2);
assert_ne!(key1, create_text_run_key("other", 0, 12.0));
assert_ne!(key1, create_text_run_key("test", 1, 12.0));
assert_ne!(key1, create_text_run_key("test", 0, 14.0));
}
#[test]
fn test_bucket_eviction() {
let mut cache = TextRunCache::new();
let base_key: u64 = 42;
for i in 0..=BUCKET_SIZE {
let key = base_key + (i as u64 * NUM_BUCKETS as u64);
cache.insert(key, create_cached_text_run(vec![], 0, 12.0, false));
}
assert!(cache.get(&base_key).is_none());
let last = base_key + (BUCKET_SIZE as u64 * NUM_BUCKETS as u64);
assert!(cache.get(&last).is_some());
}
#[test]
fn test_lru_promotion() {
let mut cache = TextRunCache::new();
let base_key: u64 = 42;
for i in 0..BUCKET_SIZE {
let key = base_key + (i as u64 * NUM_BUCKETS as u64);
cache.insert(key, create_cached_text_run(vec![], 0, 12.0, false));
}
assert!(cache.get(&base_key).is_some());
let new_key = base_key + (BUCKET_SIZE as u64 * NUM_BUCKETS as u64);
cache.insert(new_key, create_cached_text_run(vec![], 0, 12.0, false));
assert!(cache.get(&base_key).is_some());
let second = base_key + NUM_BUCKETS as u64;
assert!(cache.get(&second).is_none());
}
#[test]
fn test_clear() {
let mut cache = TextRunCache::new();
for i in 0..10 {
let key = create_text_run_key(&format!("text{i}"), 0, 12.0);
cache.insert(key, create_cached_text_run(vec![], 0, 12.0, false));
}
cache.clear();
let key = create_text_run_key("text0", 0, 12.0);
assert!(cache.get(&key).is_none());
}
}