mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
180 lines
5.7 KiB
Rust
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)));
|
|
}
|
|
}
|