Files
photocraft/crates/engine/src/cmd/smart_cmds/stack_cache.rs
T
Khawar Zamman Wani e96386fe47 Bound and purge the decoded smart object source cache
The cache estimated an entry at channels * 4 bytes a pixel, four times
too much for 8-bit, always kept a single oversize entry, recovered a
poisoned lock by disabling itself, and nothing ever cleared it.

Count each entry's real tile bytes once when it is cached, and do not
cache a source larger than the whole budget (as the stage cache does).
Web builds keep 4 entries and 384 MiB instead of 32 and 1 GiB, and the
smart filter stage cache 256 MiB instead of 768 MiB. A poisoned lock
is recovered rather than ignored.

Edit > Purge > All now clears the cache and reports it, and enables on
it alone. Closing a document that has smart objects clears it too:
entries are shared by content hash, so other documents just decode
again on their next re-render.
2026-10-29 10:03:25 +05:30

100 lines
3.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Stages of smart object renders: the placed source and the result after each visible smart
//! filter, keyed by everything that produced them. A re-render starts from the deepest cached
//! stage, so editing filter `i` only reruns the filters from `i` up: hiding or tweaking the top
//! filter of a long stack doesn't rerun the expensive filters under it.
use std::sync::{Mutex, PoisonError};
use photocraft_color::PixelFormat;
use photocraft_doc::{SmartFilter, SmartObject};
use photocraft_geom::Rect;
use photocraft_raster::Surface;
/// blake3 of a stage's inputs: the source contents, the placement and the filters so far.
pub(super) type StageKey = [u8; 32];
#[derive(Clone)]
pub(super) struct Stage {
pub surface: Surface,
/// Where filters repeat edge pixels: the canvas ∪ the placed content.
pub extent: Rect,
}
struct Entry {
key: StageKey,
stage: Stage,
bytes: usize,
}
/// Most recently used last.
static STAGES: Mutex<Vec<Entry>> = Mutex::new(Vec::new());
const ENTRIES: usize = 64;
/// Accounted tile bytes; a stage larger than this on its own isn't kept.
#[cfg(not(target_arch = "wasm32"))]
pub const BUDGET: usize = 768 << 20;
#[cfg(target_arch = "wasm32")]
pub const BUDGET: usize = 256 << 20;
/// The first stage: what placing `bytes` (the source contents) through the smart object's
/// transform, warp or perspective gives at pixel format `fmt` on `canvas`.
pub(super) fn placed_key(bytes: &[u8], fmt: PixelFormat, sm: &SmartObject, canvas: Rect) -> StageKey {
let mut h = blake3::Hasher::new();
h.update(bytes);
for v in sm.transform.m {
h.update(&v.to_bits().to_le_bytes());
}
h.update(format!("{fmt:?}|{:?}|{:?}|{:?}|{canvas:?}", sm.warp, sm.perspective, sm.stack_mode).as_bytes());
*h.finalize().as_bytes()
}
/// The stage after applying `f` to stage `below`.
pub(super) fn filter_key(below: &StageKey, f: &SmartFilter) -> StageKey {
let mut h = blake3::Hasher::new();
h.update(below);
h.update(format!("{}\0{}\0{:?}\0", f.command, f.params, f.blend).as_bytes());
h.update(&f.opacity.to_bits().to_le_bytes());
*h.finalize().as_bytes()
}
pub(super) fn get(key: &StageKey) -> Option<Stage> {
let mut c = STAGES.lock().unwrap_or_else(PoisonError::into_inner);
let i = c.iter().position(|e| e.key == *key)?;
let e = c.remove(i);
let stage = e.stage.clone();
c.push(e);
Some(stage)
}
pub(super) fn put(key: StageKey, stage: &Stage) {
let bytes = surface_bytes(&stage.surface);
if bytes > BUDGET {
return;
}
let mut c = STAGES.lock().unwrap_or_else(PoisonError::into_inner);
c.retain(|e| e.key != key);
c.push(Entry { key, stage: stage.clone(), bytes });
while c.len() > ENTRIES || c.iter().map(|e| e.bytes).sum::<usize>() > BUDGET {
c.remove(0);
}
}
/// Bytes held by cached stages.
pub fn bytes() -> usize {
STAGES.lock().unwrap_or_else(PoisonError::into_inner).iter().map(|e| e.bytes).sum()
}
/// Drop every cached stage (Edit › Purge › All). Returns the bytes released.
pub fn purge() -> usize {
let mut c = STAGES.lock().unwrap_or_else(PoisonError::into_inner);
let freed = c.iter().map(|e| e.bytes).sum();
c.clear();
freed
}
/// Bytes a surface holds: its default pixel and every tile.
pub(super) fn surface_bytes(s: &Surface) -> usize {
// Tiles shared with the document (or another stage) are charged anyway: the cache is what
// keeps them alive once the document moves on.
s.tiles().fold(s.default_bytes().len(), |n, (_, t)| n.saturating_add(t.bytes().len()))
}