Skip to content

Commit 1afdbbf

Browse files
committed
anyrender: cache converted images per item
Converting a Slint image for anyrender (RGB expansion, SVG rasterization, tile cropping) ran on every frame, and handing out a fresh peniko::Blob each time also defeats the resource caches that anyrender backends key on Blob::id. SVGs were rasterized through usvg on every single frame. Cache the conversions following the pattern of the femtovg and skia renderers: a per-item ItemCache holds the strong references (with automatic property-tracked invalidation, released in free_graphics_resources), and a shared map deduplicates conversions across items, drained by refcount once per frame. Repeating tiles stay cached as long as their source image lives; SVG entries are keyed by rasterized size. This replaces the previous global-image-cache BackendStorage round-trip, which the 5 MB global cache limit and property-cached Image values made mostly ineffective. The image goldens change because the recorded scenes now reference one deduplicated image resource instead of repeated copies.
1 parent 976c582 commit 1afdbbf

8 files changed

Lines changed: 358 additions & 198 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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

Comments
 (0)