feat: text cache has LRU eviction, memory cap
This commit is contained in:
parent
24cbc894de
commit
0192226609
1 changed files with 89 additions and 12 deletions
|
|
@ -23,10 +23,13 @@ const TEXTURE_OPTIONS: TextureOptions = TextureOptions {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Maximum texture dimension to prevent memory issues.
|
/// Maximum texture dimension to prevent memory issues.
|
||||||
const MAX_TEXTURE_DIM: u32 = 8192;
|
const MAX_TEXTURE_DIM: u32 = 16_384;
|
||||||
|
|
||||||
/// Maximum pixel count (16M pixels = 64MB for RGBA).
|
/// Maximum pixel count (256M pixels ≈ 1 GiB for RGBA).
|
||||||
const MAX_PIXEL_COUNT: usize = 16 * 1024 * 1024;
|
const MAX_PIXEL_COUNT: usize = 256 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Default memory cap for the text cache (bytes).
|
||||||
|
const DEFAULT_CACHE_BYTES: usize = 1_024 * 1_024 * 1_024; // 1 GiB
|
||||||
|
|
||||||
/// A cached rendered text texture.
|
/// A cached rendered text texture.
|
||||||
pub struct CachedText {
|
pub struct CachedText {
|
||||||
|
|
@ -45,7 +48,16 @@ pub struct TextCache {
|
||||||
/// The font used for rendering.
|
/// The font used for rendering.
|
||||||
font: FontRef<'static>,
|
font: FontRef<'static>,
|
||||||
/// Cached text textures, keyed by (content, `size_key`, render scale).
|
/// Cached text textures, keyed by (content, `size_key`, render scale).
|
||||||
cache: HashMap<(String, u32, u32), CachedText>,
|
cache: HashMap<(String, u32, u32), CachedEntry>,
|
||||||
|
max_bytes: usize,
|
||||||
|
total_bytes: usize,
|
||||||
|
usage_clock: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CachedEntry {
|
||||||
|
text: CachedText,
|
||||||
|
bytes: usize,
|
||||||
|
last_used: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextCache {
|
impl TextCache {
|
||||||
|
|
@ -66,6 +78,9 @@ impl TextCache {
|
||||||
Self {
|
Self {
|
||||||
font,
|
font,
|
||||||
cache: HashMap::new(),
|
cache: HashMap::new(),
|
||||||
|
max_bytes: DEFAULT_CACHE_BYTES,
|
||||||
|
total_bytes: 0,
|
||||||
|
usage_clock: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,10 +91,7 @@ impl TextCache {
|
||||||
|
|
||||||
/// Returns approximate GPU memory used by cached text textures (bytes).
|
/// Returns approximate GPU memory used by cached text textures (bytes).
|
||||||
pub fn cache_memory_bytes(&self) -> usize {
|
pub fn cache_memory_bytes(&self) -> usize {
|
||||||
self.cache
|
self.total_bytes
|
||||||
.values()
|
|
||||||
.map(|cached| cached.width as usize * cached.height as usize * 4)
|
|
||||||
.sum()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get or create a cached texture for the given text.
|
/// Get or create a cached texture for the given text.
|
||||||
|
|
@ -101,12 +113,49 @@ impl TextCache {
|
||||||
let render_scale_key = Self::render_scale_key(render_scale);
|
let render_scale_key = Self::render_scale_key(render_scale);
|
||||||
let key = (text.to_owned(), size_key, render_scale_key);
|
let key = (text.to_owned(), size_key, render_scale_key);
|
||||||
|
|
||||||
if !self.cache.contains_key(&key) {
|
if self.cache.contains_key(&key) {
|
||||||
let cached = self.render_text(ctx, text, nominal_font_size, size_key, render_scale);
|
self.usage_clock = self.usage_clock.wrapping_add(1);
|
||||||
self.cache.insert(key.clone(), cached);
|
if let Some(entry) = self.cache.get_mut(&key) {
|
||||||
|
entry.last_used = self.usage_clock;
|
||||||
|
}
|
||||||
|
return self
|
||||||
|
.cache
|
||||||
|
.get(&key)
|
||||||
|
.map(|entry| &entry.text)
|
||||||
|
.expect("entry should exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cache.get(&key).expect("just inserted")
|
let mut cached = self.render_text(ctx, text, nominal_font_size, size_key, render_scale);
|
||||||
|
let mut bytes = texture_bytes(&cached);
|
||||||
|
|
||||||
|
if bytes > self.max_bytes {
|
||||||
|
log::warn!(
|
||||||
|
"Text texture {} bytes exceeds cache cap {}; using empty texture",
|
||||||
|
bytes,
|
||||||
|
self.max_bytes
|
||||||
|
);
|
||||||
|
cached = Self::create_empty_texture(ctx, size_key, cached.render_scale);
|
||||||
|
bytes = texture_bytes(&cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evict least-recently-used entries until we have room.
|
||||||
|
self.ensure_capacity(bytes);
|
||||||
|
|
||||||
|
self.usage_clock = self.usage_clock.wrapping_add(1);
|
||||||
|
self.cache.insert(
|
||||||
|
key.clone(),
|
||||||
|
CachedEntry {
|
||||||
|
text: cached,
|
||||||
|
bytes,
|
||||||
|
last_used: self.usage_clock,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.total_bytes = self.total_bytes.saturating_add(bytes);
|
||||||
|
|
||||||
|
self.cache
|
||||||
|
.get(&key)
|
||||||
|
.map(|entry| &entry.text)
|
||||||
|
.expect("just inserted")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pick_render_scale(nominal_font_size: f32, zoom: f32, pixels_per_point: f32) -> f32 {
|
fn pick_render_scale(nominal_font_size: f32, zoom: f32, pixels_per_point: f32) -> f32 {
|
||||||
|
|
@ -392,4 +441,32 @@ impl TextCache {
|
||||||
render_scale,
|
render_scale,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_capacity(&mut self, incoming_bytes: usize) {
|
||||||
|
if incoming_bytes > self.max_bytes {
|
||||||
|
log::warn!(
|
||||||
|
"Incoming text texture ({incoming_bytes} bytes) exceeds cache cap ({}) - returning empty texture",
|
||||||
|
self.max_bytes
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while self.total_bytes.saturating_add(incoming_bytes) > self.max_bytes {
|
||||||
|
if let Some((evict_key, evict_entry)) = self
|
||||||
|
.cache
|
||||||
|
.iter()
|
||||||
|
.min_by_key(|(_, entry)| entry.last_used)
|
||||||
|
.map(|(k, v)| (k.clone(), v.bytes))
|
||||||
|
{
|
||||||
|
self.cache.remove(&evict_key);
|
||||||
|
self.total_bytes = self.total_bytes.saturating_sub(evict_entry);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_bytes(cached: &CachedText) -> usize {
|
||||||
|
cached.width as usize * cached.height as usize * 4
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue