Files
designcraft/crates/render/src/images.rs
T
2026-10-13 19:02:10 +00:00

180 lines
5.7 KiB
Rust

//! Decoded placed images, shared by every [`crate::Renderer`] in the process (canvas worker,
//! thumbnails, exports), so each asset is decoded once and each mip level built once.
//!
//! Entries are keyed by the address of the asset's encoded bytes (`Arc<Vec<u8>>`) and hold a
//! clone of that `Arc`, so the address can't be reused while cached. Least recently used levels
//! are evicted beyond a memory budget; at most `MAX_BAD` failed assets are remembered.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use vello_cpu::Pixmap;
use crate::{decode_pixmap_page, halve};
/// Decoded pixels kept (bytes).
const BUDGET: usize = 768 << 20;
/// Failed assets remembered; the oldest is forgotten (and retried if drawn again) beyond this.
const MAX_BAD: usize = 64;
struct Entry {
_bytes: Arc<Vec<u8>>,
pm: Arc<Pixmap>,
stamp: u64,
}
#[derive(Default)]
struct Cache {
/// (bytes address, PDF page, mip level).
map: HashMap<(usize, u32, u8), Entry>,
bytes: usize,
clock: u64,
/// Assets that failed to decode (don't retry every frame).
/// Value: (insertion stamp, the encoded bytes keeping the address reserved).
bad: HashMap<(usize, u32), (u64, Arc<Vec<u8>>)>,
/// The colour settings the pixels were converted with ([`designcraft_color::cms::generation`]).
colour: u64,
}
impl Cache {
/// Remember a failed asset, evicting the oldest failures beyond [`MAX_BAD`].
fn mark_bad(&mut self, key: (usize, u32), bytes: &Arc<Vec<u8>>) {
self.clock += 1;
self.bad.insert(key, (self.clock, bytes.clone()));
while self.bad.len() > MAX_BAD {
let Some(oldest) = self.bad.iter().min_by_key(|(_, (stamp, _))| *stamp).map(|(k, _)| *k) else { break };
self.bad.remove(&oldest);
}
}
}
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
fn size_of(pm: &Pixmap) -> usize {
pm.width() as usize * pm.height() as usize * 4
}
fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R {
let mut g = CACHE.lock().unwrap_or_else(|e| e.into_inner());
let c = g.get_or_insert_with(Cache::default);
// Decoding converts CMYK through the working space: new colour settings, new pixels.
let colour = designcraft_color::cms::generation();
if c.colour != colour {
*c = Cache { clock: c.clock, colour, ..Cache::default() };
}
f(c)
}
fn lookup(key: (usize, u32, u8)) -> Option<Arc<Pixmap>> {
with(|c| {
c.clock += 1;
let clock = c.clock;
c.map.get_mut(&key).map(|e| {
e.stamp = clock;
e.pm.clone()
})
})
}
fn store(bytes: &Arc<Vec<u8>>, page: u32, level: u8, pm: Arc<Pixmap>) {
with(|c| {
c.clock += 1;
let key = (Arc::as_ptr(bytes) as usize, page, level);
let n = size_of(&pm);
if let Some(old) = c.map.insert(key, Entry { _bytes: bytes.clone(), pm, stamp: c.clock }) {
c.bytes -= size_of(&old.pm);
}
c.bytes += n;
if c.bytes > BUDGET {
let mut v: Vec<(u64, (usize, u32, u8))> = c.map.iter().map(|(k, e)| (e.stamp, *k)).collect();
v.sort_unstable();
for (_, k) in v {
if c.bytes <= BUDGET * 3 / 4 {
break;
}
if k == key {
continue;
}
if let Some(e) = c.map.remove(&k) {
c.bytes -= size_of(&e.pm);
}
}
}
});
}
/// Full-resolution decode of `bytes` (cached).
pub fn decoded(bytes: &Arc<Vec<u8>>, page: u32) -> Option<Arc<Pixmap>> {
let id = Arc::as_ptr(bytes) as usize;
if let Some(pm) = lookup((id, page, 0)) {
return Some(pm);
}
if with(|c| c.bad.contains_key(&(id, page))) {
return None;
}
// Decode outside the lock: other threads keep drawing cached images meanwhile.
match decode_pixmap_page(bytes, page) {
Some(pm) => {
let pm = Arc::new(pm);
store(bytes, page, 0, pm.clone());
Some(pm)
}
None => {
with(|c| c.mark_bad((id, page), bytes));
None
}
}
}
/// The mip level of `bytes` closest above `on_screen` device pixels per point for a graphic
/// `width_pt` points wide.
pub fn mip(bytes: &Arc<Vec<u8>>, page: u32, width_pt: f64, on_screen: f64) -> Option<Arc<Pixmap>> {
let full = decoded(bytes, page)?;
let ppt = full.width() as f64 / width_pt.max(1e-6);
let id = Arc::as_ptr(bytes) as usize;
let mut pm = full;
let mut level = 0u8;
while ppt / (1u64 << (level + 1)) as f64 >= on_screen * 1.2 && pm.width() > 64 && pm.height() > 64 && level < 8 {
level += 1;
pm = match lookup((id, page, level)) {
Some(p) => p,
None => {
let half = Arc::new(halve(&pm));
store(bytes, page, level, half.clone());
half
}
};
}
Some(pm)
}
/// Drop every decoded image.
pub fn clear() {
with(|c| {
c.map.clear();
c.bad.clear();
c.bytes = 0;
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn failed_assets_are_capped_and_oldest_is_dropped() {
let mut c = Cache::default();
let buffers: Vec<Arc<Vec<u8>>> = (0..MAX_BAD + 10).map(|i| Arc::new(vec![i as u8; 4])).collect();
for b in &buffers {
c.mark_bad((Arc::as_ptr(b) as usize, 0), b);
}
assert_eq!(c.bad.len(), MAX_BAD);
let first = &buffers[0];
assert!(!c.bad.contains_key(&(Arc::as_ptr(first) as usize, 0)));
assert_eq!(Arc::strong_count(first), 1, "evicted buffer must be released");
let last = &buffers[MAX_BAD + 9];
assert!(c.bad.contains_key(&(Arc::as_ptr(last) as usize, 0)));
}
}