use crate::gpu_stats::FrameStats;
use crate::offscreen::OffscreenTarget;
use crate::surface_executor::{offscreen_byte_size, CachedLayerSurface};
use cranpose_render_common::bounded_lru_cache::BoundedLruCache;
use cranpose_render_common::raster_cache::{LayerRasterCacheIdentity, LayerRasterCacheKey};
use cranpose_ui_graphics::Rect;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
const MAX_LAYER_SURFACE_CACHE_ITEMS: usize = 256;
pub(crate) const MAX_LAYER_SURFACE_CACHE_BYTES: u64 = 64 * 1024 * 1024;
const MAX_SCENE_RANGE_CACHE_ITEMS: usize = 16;
pub(crate) const MAX_SCENE_RANGE_CACHE_ENTRY_BYTES: u64 = 8 * 1024 * 1024;
pub(crate) const MAX_SCENE_RANGE_CACHE_BYTES: u64 = 32 * 1024 * 1024;
const RETAINED_LAYER_SEEN_THIS_FRAME_CAPACITY: usize = 256;
pub(crate) struct LayerSurfaceCache {
entries: BoundedLruCache<LayerRasterCacheKey, CachedLayerSurface>,
scene_range_entries: BoundedLruCache<LayerRasterCacheKey, CachedLayerSurface>,
identity: HashMap<LayerRasterCacheIdentity, LayerRasterCacheKey>,
bytes: u64,
scene_range_bytes: u64,
seen_this_frame: HashSet<usize>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct LayerSurfaceCacheDebugStats {
pub(crate) entries_len: usize,
pub(crate) entries_cap: usize,
pub(crate) scene_range_entries_len: usize,
pub(crate) scene_range_entries_cap: usize,
pub(crate) identity_len: usize,
pub(crate) identity_cap: usize,
pub(crate) seen_this_frame_len: usize,
pub(crate) seen_this_frame_cap: usize,
}
impl LayerSurfaceCache {
pub(crate) fn new() -> Self {
Self {
entries: BoundedLruCache::with_capacity_at_least_one(MAX_LAYER_SURFACE_CACHE_ITEMS),
scene_range_entries: BoundedLruCache::with_capacity_at_least_one(
MAX_SCENE_RANGE_CACHE_ITEMS,
),
identity: HashMap::new(),
bytes: 0,
scene_range_bytes: 0,
seen_this_frame: HashSet::new(),
}
}
pub(crate) fn get(
&mut self,
key: &LayerRasterCacheKey,
frame_stats: &FrameStats,
) -> Option<(Rc<OffscreenTarget>, Rect)> {
if let Some(stable_id) = key.stable_id() {
self.seen_this_frame.insert(stable_id);
}
let cached = if key.is_scene_range() {
self.scene_range_entries.get(key)?
} else {
self.entries.get(key)?
};
let (width, height) = key.pixel_size();
frame_stats.record_layer_cache_hit(width, height);
Some((cached.target.clone(), cached.logical_rect))
}
pub(crate) fn insert(
&mut self,
key: LayerRasterCacheKey,
target: OffscreenTarget,
logical_rect: Rect,
frame_stats: &FrameStats,
) -> Rc<OffscreenTarget> {
if key.is_scene_range() {
return self.insert_scene_range(key, target, logical_rect, frame_stats);
}
let byte_size = offscreen_byte_size(target.width, target.height);
if let Some(stable_id) = key.stable_id() {
self.seen_this_frame.insert(stable_id);
let identity = key.identity().expect("stable cache key must have identity");
if let Some(previous_key) = self.identity.insert(identity, key) {
if previous_key != key {
self.remove(&previous_key);
}
}
}
while self.bytes + byte_size > MAX_LAYER_SURFACE_CACHE_BYTES {
let Some((evicted_key, evicted_entry)) = self.entries.pop_lru() else {
break;
};
self.bytes = self.bytes.saturating_sub(evicted_entry.byte_size);
self.remove_identity_for_key(&evicted_key);
frame_stats.record_layer_cache_eviction();
}
let cached = CachedLayerSurface {
target: Rc::new(target),
logical_rect,
byte_size,
};
let cached_handle = cached.target.clone();
if let Some((replaced_key, replaced_entry)) = self.entries.push(key, cached) {
self.bytes = self.bytes.saturating_sub(replaced_entry.byte_size);
if replaced_key != key {
frame_stats.record_layer_cache_eviction();
}
self.remove_identity_for_key(&replaced_key);
}
self.bytes = self.bytes.saturating_add(byte_size);
cached_handle
}
pub(crate) fn finish_frame(&mut self, frame_stats: &FrameStats) {
self.seen_this_frame.clear();
if self.seen_this_frame.capacity() > RETAINED_LAYER_SEEN_THIS_FRAME_CAPACITY {
self.seen_this_frame
.shrink_to(RETAINED_LAYER_SEEN_THIS_FRAME_CAPACITY);
}
self.identity.retain(|_, key| self.entries.contains(key));
frame_stats
.layer_cache_size
.set((self.entries.len() + self.scene_range_entries.len()) as u32);
frame_stats
.layer_cache_bytes
.set(self.bytes + self.scene_range_bytes);
}
pub(crate) fn debug_stats(&self) -> LayerSurfaceCacheDebugStats {
LayerSurfaceCacheDebugStats {
entries_len: self.entries.len(),
entries_cap: self.entries.cap().get(),
scene_range_entries_len: self.scene_range_entries.len(),
scene_range_entries_cap: self.scene_range_entries.cap().get(),
identity_len: self.identity.len(),
identity_cap: self.identity.capacity(),
seen_this_frame_len: self.seen_this_frame.len(),
seen_this_frame_cap: self.seen_this_frame.capacity(),
}
}
fn remove(&mut self, key: &LayerRasterCacheKey) {
if key.is_scene_range() {
self.remove_scene_range(key);
return;
}
let Some(entry) = self.entries.pop(key) else {
return;
};
self.bytes = self.bytes.saturating_sub(entry.byte_size);
self.remove_identity_for_key(key);
}
fn remove_identity_for_key(&mut self, key: &LayerRasterCacheKey) {
if let Some(identity) = key.identity() {
if self.identity.get(&identity) == Some(key) {
self.identity.remove(&identity);
}
}
}
fn insert_scene_range(
&mut self,
key: LayerRasterCacheKey,
target: OffscreenTarget,
logical_rect: Rect,
frame_stats: &FrameStats,
) -> Rc<OffscreenTarget> {
let byte_size = offscreen_byte_size(target.width, target.height);
while self.scene_range_bytes + byte_size > MAX_SCENE_RANGE_CACHE_BYTES {
let Some((_, evicted_entry)) = self.scene_range_entries.pop_lru() else {
break;
};
self.scene_range_bytes = self
.scene_range_bytes
.saturating_sub(evicted_entry.byte_size);
frame_stats.record_layer_cache_eviction();
}
let cached = CachedLayerSurface {
target: Rc::new(target),
logical_rect,
byte_size,
};
let cached_handle = cached.target.clone();
if let Some((_, replaced_entry)) = self.scene_range_entries.push(key, cached) {
self.scene_range_bytes = self
.scene_range_bytes
.saturating_sub(replaced_entry.byte_size);
frame_stats.record_layer_cache_eviction();
}
self.scene_range_bytes = self.scene_range_bytes.saturating_add(byte_size);
cached_handle
}
fn remove_scene_range(&mut self, key: &LayerRasterCacheKey) {
let Some(entry) = self.scene_range_entries.pop(key) else {
return;
};
self.scene_range_bytes = self.scene_range_bytes.saturating_sub(entry.byte_size);
}
}
impl Default for LayerSurfaceCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::{MAX_SCENE_RANGE_CACHE_BYTES, MAX_SCENE_RANGE_CACHE_ENTRY_BYTES};
use crate::surface_executor::offscreen_byte_size;
#[test]
fn scene_range_cache_keeps_multiple_visible_scale_buckets() {
let scaled_shader_ranges = [(1572, 66), (1181, 1198), (328, 390), (514, 268), (475, 189)];
let unscaled_shader_ranges = [(1160, 48), (872, 885), (242, 288), (380, 198), (351, 139)];
let required_bytes: u64 = scaled_shader_ranges
.into_iter()
.chain(unscaled_shader_ranges)
.map(|(width, height)| {
let bytes = offscreen_byte_size(width, height);
assert!(
bytes <= MAX_SCENE_RANGE_CACHE_ENTRY_BYTES,
"each reusable range must fit the per-entry scene-range budget"
);
bytes
})
.sum();
assert!(
required_bytes <= MAX_SCENE_RANGE_CACHE_BYTES,
"shader/backdrop drag should not evict reusable scene ranges between scale buckets: required={required_bytes} budget={MAX_SCENE_RANGE_CACHE_BYTES}"
);
}
}