use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use parking_lot::Mutex;
use rustc_hash::{FxHashMap, FxHasher};
use crate::{FontHandle, ShapedLine};
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
struct CacheKey {
text_hash: u64,
byte_len: usize,
font: FontHandle,
}
pub struct LineLayoutCache {
frames: [Mutex<FxHashMap<CacheKey, Arc<ShapedLine>>>; 2],
current_idx: AtomicUsize,
}
impl LineLayoutCache {
pub fn new() -> Self {
Self {
frames: [
Mutex::new(FxHashMap::default()),
Mutex::new(FxHashMap::default()),
],
current_idx: AtomicUsize::new(0),
}
}
pub fn get_or_shape<F>(&self, text: &str, font: FontHandle, shape_fn: F) -> Arc<ShapedLine>
where
F: FnOnce() -> ShapedLine,
{
let key = CacheKey {
text_hash: hash_text(text),
byte_len: text.len(),
font,
};
let current = self.current_idx.load(Ordering::Acquire);
let previous = 1 - current;
{
let current_frame = self.frames[current].lock();
if let Some(line) = current_frame.get(&key) {
return Arc::clone(line);
}
}
{
let previous_frame = self.frames[previous].lock();
if let Some(line) = previous_frame.get(&key) {
let line = Arc::clone(line);
drop(previous_frame);
self.frames[current].lock().insert(key, line.clone());
return line;
}
}
let line = Arc::new(shape_fn());
self.frames[current].lock().insert(key, Arc::clone(&line));
line
}
pub fn get_by_hash(
&self,
text_hash: u64,
byte_len: usize,
font: FontHandle,
) -> Option<Arc<ShapedLine>> {
let key = CacheKey {
text_hash,
byte_len,
font,
};
let current = self.current_idx.load(Ordering::Acquire);
let previous = 1 - current;
if let Some(line) = self.frames[current].lock().get(&key) {
return Some(Arc::clone(line));
}
if let Some(line) = self.frames[previous].lock().get(&key) {
return Some(Arc::clone(line));
}
None
}
pub fn finish_frame(&self) {
let current = self.current_idx.load(Ordering::Acquire);
let next = 1 - current;
self.frames[next].lock().clear();
self.current_idx.store(next, Ordering::Release);
}
#[cfg(test)]
pub fn len(&self) -> (usize, usize) {
let current = self.current_idx.load(Ordering::Acquire);
let previous = 1 - current;
(
self.frames[current].lock().len(),
self.frames[previous].lock().len(),
)
}
}
impl Default for LineLayoutCache {
fn default() -> Self {
Self::new()
}
}
pub fn hash_text(text: &str) -> u64 {
let mut h = FxHasher::default();
text.hash(&mut h);
h.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ShapedGlyph;
use crate::types::FontId;
fn make_font_handle() -> FontHandle {
FontHandle::from_face_id(0x1000, 16.0, 1.0)
}
fn make_shaped_line(width: f32) -> ShapedLine {
ShapedLine {
glyphs: vec![ShapedGlyph {
glyph_id: 1,
font_id: FontId::PRIMARY,
font_handle: Default::default(),
x_advance_lpx: width,
position_lpx: [0.0, 0.0],
cluster: 0,
direction: crate::types::Direction::Ltr,
}],
width_lpx: width,
ascent_lpx: 12.0,
descent_lpx: -4.0,
y_offset_lpx: 0.0,
base_direction: crate::types::Direction::Ltr,
runs: Vec::new(),
}
}
#[test]
fn cache_hit_same_frame() {
let cache = LineLayoutCache::new();
let font = make_font_handle();
let mut calls = 0;
let line1 = cache.get_or_shape("hello", font, || {
calls += 1;
make_shaped_line(50.0)
});
let line2 = cache.get_or_shape("hello", font, || {
calls += 1;
make_shaped_line(50.0)
});
assert_eq!(calls, 1, "shape_fn should only be called once");
assert!(Arc::ptr_eq(&line1, &line2), "should return same Arc");
}
#[test]
fn cache_hit_after_frame_swap() {
let cache = LineLayoutCache::new();
let font = make_font_handle();
let mut calls = 0;
let line1 = cache.get_or_shape("hello", font, || {
calls += 1;
make_shaped_line(50.0)
});
cache.finish_frame();
let line2 = cache.get_or_shape("hello", font, || {
calls += 1;
make_shaped_line(50.0)
});
assert_eq!(calls, 1, "should migrate from previous frame");
assert!(Arc::ptr_eq(&line1, &line2), "should return same Arc");
}
#[test]
fn cache_miss_on_text_change() {
let cache = LineLayoutCache::new();
let font = make_font_handle();
let mut calls = 0;
cache.get_or_shape("hello", font, || {
calls += 1;
make_shaped_line(50.0)
});
cache.get_or_shape("world", font, || {
calls += 1;
make_shaped_line(60.0)
});
assert_eq!(calls, 2, "different text should miss cache");
}
#[test]
fn entries_expire_after_two_frames() {
let cache = LineLayoutCache::new();
let font = make_font_handle();
let mut calls = 0;
cache.get_or_shape("hello", font, || {
calls += 1;
make_shaped_line(50.0)
});
cache.finish_frame();
cache.finish_frame();
cache.get_or_shape("hello", font, || {
calls += 1;
make_shaped_line(50.0)
});
assert_eq!(calls, 2, "entry should expire after two unused frames");
}
#[test]
fn get_by_hash_works() {
let cache = LineLayoutCache::new();
let font = make_font_handle();
let text = "hello";
let text_hash = hash_text(text);
cache.get_or_shape(text, font, || make_shaped_line(50.0));
let result = cache.get_by_hash(text_hash, text.len(), font);
assert!(result.is_some(), "should find by hash");
}
}