|
| 1 | +// Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 3 | + |
| 4 | +//! Shared cache of Slint images converted to [`peniko::ImageData`]. |
| 5 | +//! |
| 6 | +//! Converting a Slint image for anyrender means copying pixels (RGB |
| 7 | +//! expansion, SVG rasterization, tile cropping) and wrapping them in a |
| 8 | +//! [`peniko::Blob`]. Doing that per frame is wasteful in itself, but the |
| 9 | +//! bigger cost is downstream: anyrender backends key their own image |
| 10 | +//! resource caches on [`peniko::Blob::id`], so handing out a fresh `Blob` |
| 11 | +//! every frame forces them to re-convert (and, for vello_cpu, |
| 12 | +//! re-premultiply) the image on every fill. This cache returns the *same* |
| 13 | +//! `ImageData` for the same source image across frames, making those |
| 14 | +//! downstream caches effective. |
| 15 | +//! |
| 16 | +//! The caching model mirrors the femtovg renderer's texture cache: this |
| 17 | +//! shared map deduplicates conversions across items, while the strong |
| 18 | +//! references live in the per-item |
| 19 | +//! [`ItemCache`](i_slint_core::item_rendering::ItemCache) held by |
| 20 | +//! `AnyrenderSlintRenderer` (invalidated automatically when item |
| 21 | +//! properties change). [`ImageConversionCache::drain`], called once per |
| 22 | +//! frame, drops entries no item holds on to anymore. |
| 23 | +
|
| 24 | +use std::collections::HashMap; |
| 25 | +use std::rc::Rc; |
| 26 | + |
| 27 | +use i_slint_core::graphics::ImageCacheKey; |
| 28 | + |
| 29 | +/// A cached, converted image. Cloning is cheap and shares the underlying |
| 30 | +/// [`peniko::Blob`], keeping its id stable. |
| 31 | +pub type SharedImageData = Rc<peniko::ImageData>; |
| 32 | + |
| 33 | +/// Identifies which derived form of a source image an entry holds. |
| 34 | +#[derive(Clone, PartialEq, Eq, Hash)] |
| 35 | +pub(crate) enum ImageVariant { |
| 36 | + /// The image data as-is. |
| 37 | + Full, |
| 38 | + /// Rasterized at a specific pixel size (SVG). |
| 39 | + Sized { width: u32, height: u32 }, |
| 40 | + /// A cropped sub-rectangle used as repeating tile. The source image |
| 41 | + /// dimensions are part of the key because the crop coordinates are in |
| 42 | + /// image-data space, whose content depends on the rasterized size for |
| 43 | + /// scalable sources (SVG). |
| 44 | + Tile { source_width: u32, source_height: u32, x: u32, y: u32, width: u32, height: u32 }, |
| 45 | +} |
| 46 | + |
| 47 | +/// See the module documentation. |
| 48 | +#[derive(Default)] |
| 49 | +pub struct ImageConversionCache(HashMap<(ImageCacheKey, ImageVariant), SharedImageData>); |
| 50 | + |
| 51 | +impl ImageConversionCache { |
| 52 | + /// Look up (or convert and store) the `variant` of the image identified |
| 53 | + /// by `key`. A `None` (or [`ImageCacheKey::Invalid`]) key means the |
| 54 | + /// source has no stable identity; the conversion then runs uncached. |
| 55 | + pub(crate) fn get_or_insert( |
| 56 | + &mut self, |
| 57 | + key: Option<ImageCacheKey>, |
| 58 | + variant: ImageVariant, |
| 59 | + convert: impl FnOnce() -> Option<peniko::ImageData>, |
| 60 | + ) -> Option<SharedImageData> { |
| 61 | + let Some(key) = key.filter(|key| *key != ImageCacheKey::Invalid) else { |
| 62 | + return convert().map(Rc::new); |
| 63 | + }; |
| 64 | + match self.0.entry((key, variant)) { |
| 65 | + std::collections::hash_map::Entry::Occupied(entry) => Some(entry.get().clone()), |
| 66 | + std::collections::hash_map::Entry::Vacant(slot) => { |
| 67 | + let image_data = Rc::new(convert()?); |
| 68 | + slot.insert(image_data.clone()); |
| 69 | + Some(image_data) |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + /// Drop entries that are no longer referenced outside the cache. Call |
| 75 | + /// once per rendered frame, like the femtovg renderer's texture cache |
| 76 | + /// drain. Tiles are kept as long as their source image survives: they |
| 77 | + /// are only referenced during the frame (no item holds them), but |
| 78 | + /// re-cropping them every frame would defeat the cache for tiled |
| 79 | + /// images. |
| 80 | + pub(crate) fn drain(&mut self) { |
| 81 | + self.0.retain(|(_, variant), image_data| { |
| 82 | + matches!(variant, ImageVariant::Tile { .. }) || Rc::strong_count(image_data) > 1 |
| 83 | + }); |
| 84 | + let live_sources: std::collections::HashSet<ImageCacheKey> = self |
| 85 | + .0 |
| 86 | + .keys() |
| 87 | + .filter(|(_, variant)| !matches!(variant, ImageVariant::Tile { .. })) |
| 88 | + .map(|(key, _)| key.clone()) |
| 89 | + .collect(); |
| 90 | + self.0.retain(|(key, variant), _| { |
| 91 | + !matches!(variant, ImageVariant::Tile { .. }) || live_sources.contains(key) |
| 92 | + }); |
| 93 | + } |
| 94 | +} |
0 commit comments