Fix #2820: images drawn small redraw from their kept mips while being moved

With image mips on (the canvas always sets them), drawing an image
fetched its full decoded pixels first, decoding them again when the
shared image store no longer held them, and only then looked up the mip
it draws. The full pixels are only peeked, so they are the least
recently used entries and the first the store drops over its budget
(1 GiB on macOS and Windows). Once a document's full-size images exceed
it (20 photos of 6000 x 4000 px are 1.9 GB as RGBA), every frame decoded
every image again, though only their small mips were drawn: moving one
image redrew seconds later.

The store now remembers the size each blob's pixels decode to, so the
mip level is found without the pixels, and a mip the store holds is
drawn (and marked used) without fetching or decoding them. Without the
mip, at full size, or with mips off (exports) drawing is unchanged.

Measured on an Apple M4 (load average 15-24 from other work), 20 noise
PNGs of 6000 x 4000 px fitted in a 2880 x 1800 view, one moved 5 pt per
frame: before, 20 decodes and 1.1-3.0 s every frame; after, the first
frame decodes 20 and the later ones none, in 3-5 ms.
This commit is contained in:
Taha-Mahmoodi
2026-10-21 15:42:35 +07:00
committed by bflatastic
parent 4444450a44
commit 4e0cbfeede
4 changed files with 147 additions and 27 deletions
+23 -8
View File
@@ -311,18 +311,33 @@ pub(crate) fn cmyk_pixmap(blob: &ImageBlob, lut: &InkLut) -> Option<Pixmap> {
inks_pixmap(&blob.cmyk()?, lut)
}
/// The separation tables of placed images ([`Ink::Cmy`], [`Ink::K`]) and their generation.
fn separation() -> (Arc<[ProofLut; 2]>, u64) {
cached(&SEPARATE, |cms| {
let intent = cms.settings().intent;
[Ink::Cmy, Ink::K].map(|ink| ProofLut::build(|rgb| ink.plane(cms.srgb_to_cmyk(rgb, intent))))
})
}
/// The key of image pixels `key` painted as `plane` with the separation tables of `generation`.
fn plane_key(key: &ImageKey, plane: Ink, generation: u64) -> ImageKey {
ImageKey { ink: plane, luts: (key.luts.0, generation), ..key.clone() }
}
/// The key [`ink_image`] gives image pixels `key` painted with `ink`.
pub(crate) fn ink_key(key: &ImageKey, ink: Ink) -> ImageKey {
if matches!(ink, Ink::Display | Ink::RichBlack) { key.clone() } else { plane_key(key, ink, separation().1) }
}
/// Image pixels `pm` (cached as `key`, from `blob` when it is a document's image as it is) as
/// painted with `ink`, cached with the images, and their key.
pub(crate) fn ink_image(key: &ImageKey, pm: &Arc<Pixmap>, ink: Ink, blob: Option<&ImageBlob>, touch: bool) -> (ImageKey, Arc<Pixmap>) {
if matches!(ink, Ink::Display | Ink::RichBlack) {
return (key.clone(), pm.clone());
}
let (luts, generation) = cached(&SEPARATE, |cms| {
let intent = cms.settings().intent;
[Ink::Cmy, Ink::K].map(|ink| ProofLut::build(|rgb| ink.plane(cms.srgb_to_cmyk(rgb, intent))))
});
let plane_key = |plane: Ink| ImageKey { ink: plane, luts: (key.luts.0, generation), ..key.clone() };
let want = plane_key(ink);
let (luts, generation) = separation();
let keyed = |plane: Ink| plane_key(key, plane, generation);
let want = keyed(ink);
let hit = {
let mut s = images::store();
if touch { s.get(&want) } else { s.peek(&want) }
@@ -357,8 +372,8 @@ pub(crate) fn ink_image(key: &ImageKey, pm: &Arc<Pixmap>, ink: Ink, blob: Option
});
let [cmy, k] = planes;
let mut s = images::store();
let cmy = s.insert(plane_key(Ink::Cmy), cmy);
let k = s.insert(plane_key(Ink::K), k);
let cmy = s.insert(keyed(Ink::Cmy), cmy);
let k = s.insert(keyed(Ink::K), k);
(want, if ink == Ink::K { k } else { cmy })
}