cranpose-render-wgpu 0.1.14

WGPU renderer backend for Cranpose
Documentation
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}"
        );
    }
}