blob: f201352220c96ce091b4c46c5ed6ab9fa7e81662 [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
#![allow(dead_code, reason = "Clippy fails when --no-default-features")]
use alloc::vec::Vec;
use guillotiere::{AllocId, AtlasAllocator, size2};
use vello_common::paint::ImageId;
const DEFAULT_ATLAS_SIZE: u32 = 1024;
/// Represents an image resource for rendering
#[derive(Debug)]
pub(crate) struct ImageResource {
/// The ID of the image
pub(crate) id: ImageId,
/// The width of the image
pub(crate) width: u16,
/// The height of the image
pub(crate) height: u16,
/// The offset of the image in the atlas
pub(crate) offset: [u16; 2],
/// The atlas allocation ID for deallocation
atlas_alloc_id: AllocId,
}
/// Manages image resources for the renderer
pub(crate) struct ImageCache {
/// Atlas allocator for the images
atlas: AtlasAllocator,
/// Vector of optional image resources (None = free slot)
slots: Vec<Option<ImageResource>>,
/// Stack of free indices for O(1) allocation/deallocation
free_idxs: Vec<usize>,
}
impl core::fmt::Debug for ImageCache {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
// Count allocated and free rectangles in the atlas
let mut allocated_count = 0;
let mut free_count = 0;
self.atlas.for_each_allocated_rectangle(|_id, _rect| {
allocated_count += 1;
});
self.atlas.for_each_free_rectangle(|_rect| {
free_count += 1;
});
f.debug_struct("ImageCache")
.field("slots", &self.slots)
.field("free_idxs", &self.free_idxs)
.field("atlas_size", &self.atlas.size())
.field("atlas_is_empty", &self.atlas.is_empty())
.field("atlas_allocated_count", &allocated_count)
.field("atlas_free_count", &free_count)
.finish()
}
}
impl Default for ImageCache {
fn default() -> Self {
Self::new(DEFAULT_ATLAS_SIZE, DEFAULT_ATLAS_SIZE)
}
}
impl ImageCache {
/// Create a new image cache
pub(crate) fn new(width: u32, height: u32) -> Self {
Self {
atlas: AtlasAllocator::new(size2(width as i32, height as i32)),
slots: Vec::new(),
free_idxs: Vec::new(),
}
}
/// Get an image resource by its Id
pub(crate) fn get(&self, id: ImageId) -> Option<&ImageResource> {
self.slots.get(id.as_u32() as usize)?.as_ref()
}
/// Check if an image is in the cache
pub(crate) fn contains(&self, id: ImageId) -> bool {
self.slots
.get(id.as_u32() as usize)
.and_then(|slot| slot.as_ref())
.is_some()
}
/// Allocate an image in the cache
#[expect(
clippy::cast_possible_truncation,
reason = "u16 is enough for the offset and width/height"
)]
pub(crate) fn allocate(&mut self, width: u32, height: u32) -> ImageId {
let alloc = self
.atlas
.allocate(size2(width as i32, height as i32))
.expect("Failed to allocate image in atlas");
let slot_idx = self.free_idxs.pop().unwrap_or_else(|| {
// No free slots, append to vector
let index = self.slots.len();
// Placeholder, will be replaced
self.slots.push(None);
index
});
let image_id = ImageId::new(slot_idx as u32);
self.slots[slot_idx] = Some(ImageResource {
id: image_id,
width: width as u16,
height: height as u16,
offset: [alloc.rectangle.min.x as u16, alloc.rectangle.min.y as u16],
atlas_alloc_id: alloc.id,
});
image_id
}
/// Deallocate an image from the cache, returning true if it existed
pub(crate) fn deallocate(&mut self, id: ImageId) -> Option<ImageResource> {
let index = id.as_u32() as usize;
if let Some(image_resource) = self.slots.get_mut(index).and_then(Option::take) {
// Deallocate from the atlas using the stored allocation ID
self.atlas.deallocate(image_resource.atlas_alloc_id);
self.free_idxs.push(index);
Some(image_resource)
} else {
None
}
}
/// Clear all images from the cache
pub(crate) fn clear(&mut self) {
self.slots.clear();
self.free_idxs.clear();
self.atlas.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_single_image() {
let mut cache = ImageCache::default();
let id = cache.allocate(100, 100);
assert_eq!(id.as_u32(), 0);
let resource = cache.get(id).unwrap();
assert_eq!(resource.id, id);
assert_eq!(resource.width, 100);
assert_eq!(resource.height, 100);
assert_eq!(resource.offset, [0, 0]); // First image should be at origin
}
#[test]
fn test_insert_multiple_images() {
let mut cache = ImageCache::default();
let id1 = cache.allocate(50, 50);
let id2 = cache.allocate(75, 75);
assert_eq!(id1.as_u32(), 0);
assert_eq!(id2.as_u32(), 1);
let resource1 = cache.get(id1).unwrap();
let resource2 = cache.get(id2).unwrap();
assert_eq!(resource1.width, 50);
assert_eq!(resource2.width, 75);
// Second image should be placed adjacent to first
assert_ne!(resource1.offset, resource2.offset);
}
#[test]
fn test_get_nonexistent_image() {
let cache: ImageCache = ImageCache::default();
assert!(cache.get(ImageId::new(0)).is_none());
assert!(cache.get(ImageId::new(999)).is_none());
}
#[test]
fn test_remove_image() {
let mut cache = ImageCache::default();
let id = cache.allocate(100, 100);
assert!(cache.get(id).is_some());
cache.deallocate(id);
assert!(cache.get(id).is_none());
}
#[test]
fn test_remove_nonexistent_image() {
let mut cache: ImageCache = ImageCache::default();
// Should not panic when unregistering non-existent image
cache.deallocate(ImageId::new(0));
cache.deallocate(ImageId::new(999));
}
#[test]
fn test_slot_reuse_after_remove() {
let mut cache = ImageCache::default();
// Register three images
let id1 = cache.allocate(50, 50);
let id2 = cache.allocate(60, 60);
let id3 = cache.allocate(70, 70);
assert_eq!(id1.as_u32(), 0);
assert_eq!(id2.as_u32(), 1);
assert_eq!(id3.as_u32(), 2);
// Unregister the middle one
cache.deallocate(id2);
assert!(cache.get(id2).is_none());
// Register a new image - should reuse slot 1
let id4 = cache.allocate(80, 80);
// Reused slot 1
assert_eq!(id4.as_u32(), 1);
// Verify other images are still there
assert!(cache.get(id1).is_some());
assert!(cache.get(id3).is_some());
assert!(cache.get(id4).is_some());
assert_eq!(cache.get(id4).unwrap().width, 80);
}
#[test]
fn test_multiple_remove_and_reuse() {
let mut cache = ImageCache::default();
// Register several images
let ids: Vec<_> = (0..5)
.map(|i| cache.allocate(100 + i * 10, 100 + i * 10))
.collect();
// Unregister some in the middle
cache.deallocate(ids[1]);
cache.deallocate(ids[3]);
// Register new images - should reuse the freed slots
let new_id1 = cache.allocate(200, 200);
let new_id2 = cache.allocate(300, 300);
// Should have reused slots 3 and 1 (in reverse order due to stack behavior)
assert!(new_id1.as_u32() == 3);
assert!(new_id2.as_u32() == 1);
assert_ne!(new_id1.as_u32(), new_id2.as_u32());
}
#[test]
fn test_contains() {
let mut cache = ImageCache::default();
let id = cache.allocate(100, 100);
assert!(cache.contains(id));
cache.deallocate(id);
assert!(!cache.contains(id));
}
}