lgui-core 0.2.0

Platform-neutral runtime and UI primitives for LGUI
Documentation
use super::{memory::estimate_scene_commands_bytes, primitive::*, *};

const SCROLL_RASTER_COMMAND_CACHE_LIMIT: usize = 8;

#[derive(Clone, Hash, PartialEq, Eq)]
struct ScrollRasterCommandCacheKey {
    id: String,
    cache_epoch: u64,
    left: u32,
    top: u32,
    right: u32,
    bottom: u32,
    content_height: u32,
}

#[derive(Clone)]
struct ScrollRasterCommandSnapshot {
    commands: Vec<ScenePrimitive>,
    child_signature: u64,
    last_used: u64,
    bytes: usize,
}

struct ScrollRasterCommandCache {
    entries: HashMap<ScrollRasterCommandCacheKey, ScrollRasterCommandSnapshot>,
    tick: u64,
    bytes: usize,
    budget_bytes: usize,
    hits: u64,
    misses: u64,
    evictions: u64,
}

impl Default for ScrollRasterCommandCache {
    fn default() -> Self {
        Self {
            entries: HashMap::new(),
            tick: 0,
            bytes: 0,
            budget_bytes: 0,
            hits: 0,
            misses: 0,
            evictions: 0,
        }
    }
}

fn scroll_raster_command_cache() -> &'static Mutex<ScrollRasterCommandCache> {
    static CACHE: OnceLock<Mutex<ScrollRasterCommandCache>> = OnceLock::new();
    CACHE.get_or_init(|| Mutex::new(ScrollRasterCommandCache::default()))
}

pub(crate) fn scroll_raster_command_cache_usage() -> crate::memory::CacheUsage {
    let cache = scroll_raster_command_cache()
        .lock()
        .expect("scroll raster command cache poisoned");
    crate::memory::CacheUsage {
        rebuildable_bytes: cache.bytes,
        cpu_bytes: cache.bytes,
        entries: cache.entries.len(),
        hits: cache.hits,
        misses: cache.misses,
        evictions: cache.evictions,
        largest_entry_bytes: cache
            .entries
            .values()
            .map(|entry| entry.bytes)
            .max()
            .unwrap_or(0),
        ..Default::default()
    }
}

pub(crate) fn trim_scroll_raster_command_cache(target_bytes: usize) -> usize {
    let mut cache = scroll_raster_command_cache()
        .lock()
        .expect("scroll raster command cache poisoned");
    let before = cache.bytes;
    evict_scroll_raster_commands(&mut cache, target_bytes, SCROLL_RASTER_COMMAND_CACHE_LIMIT);
    before.saturating_sub(cache.bytes)
}

pub(crate) fn set_scroll_raster_command_cache_budget(budget_bytes: usize) {
    let mut cache = scroll_raster_command_cache()
        .lock()
        .expect("scroll raster command cache poisoned");
    cache.budget_bytes = budget_bytes;
    let budget = cache.budget_bytes;
    evict_scroll_raster_commands(&mut cache, budget, SCROLL_RASTER_COMMAND_CACHE_LIMIT);
}

pub(super) fn load_scroll_raster_command_snapshot(
    node: &UiNode,
    spec: &ScrollRasterSpec,
) -> Option<(Vec<ScenePrimitive>, u64)> {
    let key = scroll_raster_command_cache_key(node, spec);
    let mut cache = scroll_raster_command_cache()
        .lock()
        .expect("scroll raster command cache poisoned");
    cache.tick = cache.tick.saturating_add(1);
    let tick = cache.tick;
    if !cache.entries.contains_key(&key) {
        cache.misses = cache.misses.saturating_add(1);
        return None;
    }
    cache.hits = cache.hits.saturating_add(1);
    let snapshot = cache
        .entries
        .get_mut(&key)
        .expect("scroll raster command cache key vanished");
    snapshot.last_used = tick;
    Some((snapshot.commands.clone(), snapshot.child_signature))
}

pub fn scroll_raster_command_snapshot_exists(
    id: &UiId,
    rect: UiRect,
    spec: &ScrollRasterSpec,
) -> bool {
    let key = scroll_raster_command_cache_key_for(id, rect, spec);
    scroll_raster_command_cache()
        .lock()
        .expect("scroll raster command cache poisoned")
        .entries
        .contains_key(&key)
}

pub(super) fn store_scroll_raster_command_snapshot(
    node: &UiNode,
    spec: &ScrollRasterSpec,
    commands: Vec<ScenePrimitive>,
    child_signature: u64,
) {
    let key = scroll_raster_command_cache_key(node, spec);
    let bytes = estimate_scene_commands_bytes(&commands);
    let mut cache = scroll_raster_command_cache()
        .lock()
        .expect("scroll raster command cache poisoned");
    if bytes > cache.budget_bytes {
        return;
    }
    cache.tick = cache.tick.saturating_add(1);
    let tick = cache.tick;
    if let Some(previous) = cache.entries.remove(&key) {
        cache.bytes = cache.bytes.saturating_sub(previous.bytes);
    }
    cache.bytes = cache.bytes.saturating_add(bytes);
    cache.entries.insert(
        key,
        ScrollRasterCommandSnapshot {
            commands,
            child_signature,
            last_used: tick,
            bytes,
        },
    );
    let budget = cache.budget_bytes;
    evict_scroll_raster_commands(&mut cache, budget, SCROLL_RASTER_COMMAND_CACHE_LIMIT);
}

fn evict_scroll_raster_commands(
    cache: &mut ScrollRasterCommandCache,
    target_bytes: usize,
    target_entries: usize,
) {
    while cache.bytes > target_bytes || cache.entries.len() > target_entries {
        let Some(oldest_key) = cache
            .entries
            .iter()
            .min_by_key(|(_, snapshot)| snapshot.last_used)
            .map(|(key, _)| key.clone())
        else {
            break;
        };
        if let Some(entry) = cache.entries.remove(&oldest_key) {
            cache.bytes = cache.bytes.saturating_sub(entry.bytes);
            cache.evictions = cache.evictions.saturating_add(1);
        }
    }
}

fn scroll_raster_command_cache_key(
    node: &UiNode,
    spec: &ScrollRasterSpec,
) -> ScrollRasterCommandCacheKey {
    scroll_raster_command_cache_key_for(&node.id, node.layout_rect, spec)
}

fn scroll_raster_command_cache_key_for(
    id: &UiId,
    rect: UiRect,
    spec: &ScrollRasterSpec,
) -> ScrollRasterCommandCacheKey {
    ScrollRasterCommandCacheKey {
        id: id.as_str().to_string(),
        cache_epoch: spec.cache_epoch,
        left: normalized_f32_bits(rect.left),
        top: normalized_f32_bits(rect.top),
        right: normalized_f32_bits(rect.right),
        bottom: normalized_f32_bits(rect.bottom),
        content_height: normalized_f32_bits(spec.content_height),
    }
}