| // Copyright 2022 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| use guillotiere::{AllocId, AtlasAllocator, size2}; |
| use peniko::ImageData; |
| use std::collections::HashMap; |
| use std::collections::hash_map::Entry; |
| |
| const DEFAULT_ATLAS_SIZE: i32 = 1024; |
| const MAX_ATLAS_SIZE: i32 = 8192; |
| const EVICT_AFTER_GENERATIONS: u64 = 2; |
| |
| #[derive(Default)] |
| pub struct Images<'a> { |
| /// Width of the square image atlas texture. |
| pub width: u32, |
| /// Height of the square image atlas texture. |
| pub height: u32, |
| /// Number of resident images evicted during the current resolve pass. |
| /// |
| /// This is only used for renderer-side debug logging. |
| pub evicted: usize, |
| /// Images that must be uploaded in the current resolve pass, with atlas locations. |
| pub images: &'a [(ImageData, u32, u32)], |
| } |
| |
| #[derive(Clone)] |
| struct ResidentImage { |
| image: ImageData, |
| alloc_id: AllocId, |
| x: u32, |
| y: u32, |
| dirty: bool, |
| last_used_generation: u64, |
| } |
| |
| pub(crate) struct ImageCache { |
| atlas: AtlasAllocator, |
| /// Maximum side length for the square image atlas texture. |
| max_size: i32, |
| /// Monotonic counter for resolve passes, used to track when resident images were last used. |
| generation: u64, |
| /// Number of resident images evicted during the current resolve pass. |
| /// |
| /// This is exposed through [`Images::evicted`] for renderer-side debug logging, and also |
| /// prevents repeated stale-eviction scans during the same resolve pass. |
| evicted_in_resolve: usize, |
| /// Map from image blob id to atlas residency. |
| map: HashMap<u64, ResidentImage>, |
| /// Images that must be uploaded in the current resolve pass, with atlas locations. |
| images: Vec<(ImageData, u32, u32)>, |
| } |
| |
| impl Default for ImageCache { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl ImageCache { |
| pub(crate) fn new() -> Self { |
| Self::new_with_sizes(DEFAULT_ATLAS_SIZE, MAX_ATLAS_SIZE) |
| } |
| |
| fn new_with_sizes(initial_size: i32, max_size: i32) -> Self { |
| Self { |
| atlas: AtlasAllocator::new(size2(initial_size, initial_size)), |
| max_size, |
| generation: 0, |
| evicted_in_resolve: 0, |
| map: HashMap::default(), |
| images: Vec::default(), |
| } |
| } |
| |
| pub(crate) fn begin_resolve(&mut self) { |
| self.generation = self.generation.wrapping_add(1); |
| self.evicted_in_resolve = 0; |
| self.images.clear(); |
| } |
| |
| pub(crate) fn restart_resolve_pass(&mut self) { |
| self.images.clear(); |
| let previous_generation = self.generation.wrapping_sub(1); |
| for resident in self.map.values_mut() { |
| if resident.last_used_generation == self.generation { |
| resident.last_used_generation = previous_generation; |
| } |
| } |
| } |
| |
| pub(crate) fn images(&self) -> Images<'_> { |
| Images { |
| width: self.atlas.size().width as u32, |
| height: self.atlas.size().height as u32, |
| evicted: self.evicted_in_resolve, |
| images: &self.images, |
| } |
| } |
| |
| pub(crate) fn bump_size(&mut self) -> bool { |
| let mut new_size = self.atlas.size().width * 2; |
| while new_size <= self.max_size { |
| if self.repack_to_size(new_size) { |
| self.images.clear(); |
| return true; |
| } |
| new_size *= 2; |
| } |
| false |
| } |
| |
| pub(crate) fn get_or_insert(&mut self, image: &ImageData) -> Option<(u32, u32)> { |
| match self.map.entry(image.data.id()) { |
| Entry::Occupied(mut occupied) => { |
| let resident = occupied.get_mut(); |
| let xy = (resident.x, resident.y); |
| if resident.last_used_generation != self.generation { |
| resident.last_used_generation = self.generation; |
| if resident.dirty { |
| self.images |
| .push((resident.image.clone(), resident.x, resident.y)); |
| } |
| } |
| Some(xy) |
| } |
| Entry::Vacant(vacant) => { |
| let alloc = self |
| .atlas |
| .allocate(size2(image.width as _, image.height as _))?; |
| let x = alloc.rectangle.min.x as u32; |
| let y = alloc.rectangle.min.y as u32; |
| let resident = ResidentImage { |
| image: image.clone(), |
| alloc_id: alloc.id, |
| x, |
| y, |
| dirty: true, |
| last_used_generation: self.generation, |
| }; |
| self.images.push((image.clone(), x, y)); |
| vacant.insert(resident); |
| Some((x, y)) |
| } |
| } |
| } |
| |
| pub(crate) fn finish_resolve(&mut self) { |
| for resident in self.map.values_mut() { |
| if resident.last_used_generation == self.generation { |
| resident.dirty = false; |
| } |
| } |
| } |
| |
| pub(crate) fn can_fit_image(&self, image: &ImageData) -> bool { |
| image.width <= self.atlas.size().width as u32 |
| && image.height <= self.atlas.size().height as u32 |
| } |
| |
| pub(crate) fn mark_dirty(&mut self, image: &ImageData) { |
| if let Some(resident) = self.map.get_mut(&image.data.id()) { |
| resident.dirty = true; |
| } |
| } |
| |
| pub(crate) fn evict_stale_entries(&mut self) -> bool { |
| if self.evicted_in_resolve != 0 { |
| return false; |
| } |
| let Some(stale_before) = self.generation.checked_sub(EVICT_AFTER_GENERATIONS) else { |
| return false; |
| }; |
| for (_id, resident) in self |
| .map |
| .extract_if(|_, resident| resident.last_used_generation < stale_before) |
| { |
| self.atlas.deallocate(resident.alloc_id); |
| self.evicted_in_resolve += 1; |
| } |
| self.evicted_in_resolve != 0 |
| } |
| |
| fn repack_to_size(&mut self, size: i32) -> bool { |
| let mut atlas = AtlasAllocator::new(size2(size, size)); |
| let mut entries: Vec<_> = self.map.iter().collect(); |
| entries.sort_by_key(|(id, _)| *id); |
| let mut map = HashMap::with_capacity(self.map.len()); |
| for (id, resident) in entries { |
| let Some(alloc) = |
| atlas.allocate(size2(resident.image.width as _, resident.image.height as _)) |
| else { |
| return false; |
| }; |
| let mut resident = resident.clone(); |
| resident.alloc_id = alloc.id; |
| resident.x = alloc.rectangle.min.x as u32; |
| resident.y = alloc.rectangle.min.y as u32; |
| resident.dirty = true; |
| map.insert(*id, resident); |
| } |
| self.atlas = atlas; |
| self.map = map; |
| true |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use peniko::{Blob, ImageAlphaType, ImageFormat}; |
| use std::sync::Arc; |
| |
| fn image(id_byte: u8, width: u32, height: u32) -> ImageData { |
| let len = (width * height * 4) as usize; |
| ImageData { |
| data: Blob::new(Arc::new(vec![id_byte; len])), |
| format: ImageFormat::Rgba8, |
| width, |
| height, |
| alpha_type: ImageAlphaType::Alpha, |
| } |
| } |
| |
| #[test] |
| fn atlas_size_persists_after_growth() { |
| let mut cache = ImageCache::new_with_sizes(16, 64); |
| assert_eq!(cache.atlas.size().width, 16); |
| assert!(cache.bump_size()); |
| assert_eq!(cache.atlas.size().width, 32); |
| cache.begin_resolve(); |
| assert_eq!(cache.atlas.size().width, 32); |
| } |
| |
| #[test] |
| fn resident_entries_are_reused_across_resolves() { |
| let mut cache = ImageCache::new_with_sizes(32, 64); |
| let image = image(7, 8, 8); |
| |
| cache.begin_resolve(); |
| let first = cache.get_or_insert(&image).unwrap(); |
| assert_eq!(cache.images.len(), 1); |
| cache.finish_resolve(); |
| |
| cache.begin_resolve(); |
| let second = cache.get_or_insert(&image).unwrap(); |
| assert_eq!(first, second); |
| assert_eq!(cache.images.len(), 0); |
| assert_eq!(cache.map.len(), 1); |
| } |
| |
| #[test] |
| fn stale_entries_can_be_evicted_under_pressure() { |
| let mut cache = ImageCache::new_with_sizes(16, 16); |
| let image_a = image(1, 10, 10); |
| let image_b = image(2, 10, 10); |
| |
| cache.begin_resolve(); |
| assert!(cache.get_or_insert(&image_a).is_some()); |
| |
| cache.begin_resolve(); |
| |
| cache.begin_resolve(); |
| cache.begin_resolve(); |
| assert!(cache.get_or_insert(&image_b).is_none()); |
| assert!(cache.evict_stale_entries()); |
| assert!(cache.get_or_insert(&image_b).is_some()); |
| assert!(!cache.map.contains_key(&image_a.data.id())); |
| } |
| |
| #[test] |
| fn stale_entries_are_evicted_at_most_once_per_resolve() { |
| let mut cache = ImageCache::new_with_sizes(32, 32); |
| let image_a = image(1, 8, 8); |
| let image_b = image(2, 8, 8); |
| |
| cache.begin_resolve(); |
| assert!(cache.get_or_insert(&image_a).is_some()); |
| cache.finish_resolve(); |
| |
| cache.begin_resolve(); |
| assert!(cache.get_or_insert(&image_b).is_some()); |
| cache.finish_resolve(); |
| |
| cache.begin_resolve(); |
| cache.begin_resolve(); |
| cache.begin_resolve(); |
| |
| assert!(cache.evict_stale_entries()); |
| assert!(!cache.evict_stale_entries()); |
| } |
| |
| #[test] |
| fn failed_repack_leaves_existing_residency_unchanged() { |
| let mut cache = ImageCache::new_with_sizes(16, 16); |
| let image = image(1, 12, 12); |
| |
| cache.begin_resolve(); |
| let xy = cache.get_or_insert(&image).unwrap(); |
| |
| assert!(!cache.repack_to_size(8)); |
| assert_eq!(cache.atlas.size().width, 16); |
| assert_eq!(cache.get_or_insert(&image), Some(xy)); |
| assert_eq!(cache.map.len(), 1); |
| } |
| } |