///|
/// Session-scoped shaping cache (ADR 0034 batch, renderer-owned per P16):
/// every `DrawText` frame re-shapes identical `(font, text, resolution)`
/// runs. Resolved layouts are pure values plus live font handles, so an LRU
/// over them is hit-free to replay. Hit/miss deltas fold into the existing
/// `RenderFrameResult` cache counters; no protocol field changes.
///|
priv struct SkiaTextShapingEntry {
layout : SkiaResolvedTextLayout
mut last_used : Int
}
///|
priv struct SkiaTextShapingCache {
entries : Map[String, SkiaTextShapingEntry]
mut tick : Int
mut total_hits : Int
mut total_misses : Int
mut frame_mark_hits : Int
mut frame_mark_misses : Int
}
///|
let skia_text_shaping_cache_capacity = 256
///|
fn SkiaTextShapingCache::new() -> SkiaTextShapingCache {
{
entries: Map([]),
tick: 0,
total_hits: 0,
total_misses: 0,
frame_mark_hits: 0,
frame_mark_misses: 0,
}
}
///|
fn skia_text_shaping_cache_key(
font_spec : @core.FontSpec,
text : String,
resolution : SkiaFontResolution,
) -> String {
let resolution_tag = match resolution {
SystemFontMgr => "system"
EmptyTypeface => "empty"
}
font_spec.to_json().stringify() + "\{resolution_tag}\n" + text
}
///|
fn SkiaTextShapingCache::begin_frame(self : SkiaTextShapingCache) -> Unit {
self.frame_mark_hits = self.total_hits
self.frame_mark_misses = self.total_misses
}
///|
fn SkiaTextShapingCache::frame_delta(self : SkiaTextShapingCache) -> (Int, Int) {
(
self.total_hits - self.frame_mark_hits,
self.total_misses - self.frame_mark_misses,
)
}
///|
fn SkiaTextShapingCache::resolve(
self : SkiaTextShapingCache,
font_spec : @core.FontSpec,
text : String,
resolution : SkiaFontResolution,
shape : () -> SkiaResolvedTextLayout?,
) -> SkiaResolvedTextLayout? {
self.tick = self.tick + 1
let key = skia_text_shaping_cache_key(font_spec, text, resolution)
match self.entries.get(key) {
Some(entry) => {
entry.last_used = self.tick
self.total_hits = self.total_hits + 1
Some(entry.layout.copy_layout())
}
None => {
self.total_misses = self.total_misses + 1
match shape() {
Some(layout) => {
self.entries[key] = { layout, last_used: self.tick, }
self.evict_if_needed()
Some(layout)
}
None => None
}
}
}
}
///|
fn SkiaTextShapingCache::evict_if_needed(self : SkiaTextShapingCache) -> Unit {
while self.entries.length() > skia_text_shaping_cache_capacity {
let mut oldest_key : String? = None
let mut oldest_tick = self.tick + 1
for key, entry in self.entries {
if entry.last_used < oldest_tick {
oldest_key = Some(key)
oldest_tick = entry.last_used
}
}
match oldest_key {
Some(key) => ignore(self.entries.remove(key))
None => break
}
}
}