//! Drawing placed documents ([`PlacedDocument`]). //! //! Exactly: the art read from the file ([`vectorcraft_doc::placed_document::read`]) drawn with its //! own resources, through a view that maps its artboard into the object's box (nothing is copied //! into the document). //! //! On screen ([`RenderOptions::progressive_placed`]): a placed document can't be edited, so what //! it shows is cached as a bitmap per file and zoom step (a power of two, in device pixels per //! point), shared by every object showing that file. A bitmap not made yet is made on a //! background worker while the frame shows another step's bitmap, or a light placeholder; //! [`generation`] changes when one is ready and [`busy`] says whether any is coming, so the screen //! can draw again. Past [`MAX_SIDE`] pixels, and in outline, proofing and ink frames, it is drawn //! exactly. //! //! A document opened with only its placed documents' previews ([`ImageBlob::is_proxy`]) shows them //! up to their resolution; past it the file is read again in the background //! ([`vectorcraft_doc::placed_document::full_bytes`]), the preview standing in until then (and for //! good when the file can't be found). Placed documents inside a placed document are drawn the //! same way, up to [`MAX_DEPTH`] deep; deeper ones show their previews. use std::cell::Cell; use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, LazyLock, Mutex, MutexGuard, PoisonError}; use vectorcraft_doc::placed_document::MAX_DEPTH; use vectorcraft_doc::{ImageBlob, Node, PlacedDocument}; use vectorcraft_geom::{Affine, Rect}; use vello_cpu::{Pixmap, RenderContext, kurbo, peniko}; use crate::ink::Ink; use crate::{Frame, Renderer}; /// The longest side of a cached bitmap (pixels); larger steps draw exactly. pub const MAX_SIDE: f64 = 4096.0; /// The most bytes of bitmaps kept. const BUDGET: usize = 512 << 20; /// The smallest zoom step (device pixels per point). const MIN_STEP: f64 = 1.0 / 64.0; /// A bitmap's identity: the file shown (its key) and the zoom step's bits. type BitmapKey = (String, u64); struct Bitmap { pm: Arc, step: f64, used: u64, } #[derive(Default)] struct Bitmaps { map: HashMap, clock: u64, bytes: usize, } static BITMAPS: LazyLock> = LazyLock::new(Default::default); /// Bitmaps being made. static PENDING: LazyLock>> = LazyLock::new(Default::default); static GENERATION: AtomicU64 = AtomicU64::new(0); fn lock(m: &Mutex) -> MutexGuard<'_, T> { // The caches stay whole if a holder panicked (entries are only inserted and removed). m.lock().unwrap_or_else(PoisonError::into_inner) } /// Changes whenever a bitmap made in the background is ready: a screen showing placed documents /// draws again when it does. pub fn generation() -> u64 { GENERATION.load(Ordering::Relaxed) } /// Are bitmaps being made in the background? pub fn busy() -> bool { !lock(&PENDING).is_empty() } /// Is a bitmap of file `key` being made? #[cfg(test)] pub(crate) fn pending_for(key: &str) -> bool { lock(&PENDING).iter().any(|k| k.0 == key) } /// The zoom step for `scale` device pixels per point: the power of two at or above it. pub(crate) fn step(scale: f64) -> Option { (scale.is_finite() && scale > 0.0).then(|| 2f64.powf(scale.log2().ceil()).max(MIN_STEP)) } impl Bitmaps { fn get(&mut self, k: &BitmapKey) -> Option> { self.clock += 1; let clock = self.clock; let b = self.map.get_mut(k)?; b.used = clock; Some(b.pm.clone()) } /// The bitmap of another step of the same file, the nearest to `step`. fn nearest(&self, key: &str, step: f64) -> Option<(Arc, f64)> { self.map .iter() .filter(|((k, _), _)| k == key) .min_by(|a, b| (a.1.step / step).log2().abs().total_cmp(&(b.1.step / step).log2().abs())) .map(|(_, b)| (b.pm.clone(), b.step)) } fn insert(&mut self, k: BitmapKey, pm: Pixmap, step: f64) { self.clock += 1; let size = pm.width() as usize * pm.height() as usize * 4; self.bytes += size; if let Some(old) = self.map.insert(k, Bitmap { pm: Arc::new(pm), step, used: self.clock }) { self.bytes = self.bytes.saturating_sub(old.pm.width() as usize * old.pm.height() as usize * 4); } while self.bytes > BUDGET && self.map.len() > 1 { let Some(k) = self.map.iter().min_by_key(|(_, b)| b.used).map(|(k, _)| k.clone()) else { break }; if let Some(b) = self.map.remove(&k) { self.bytes = self.bytes.saturating_sub(b.pm.width() as usize * b.pm.height() as usize * 4); } } } } /// What a bitmap is made from. struct Job { key: BitmapKey, object: PlacedDocument, blob: ImageBlob, step: f64, } /// Device pixels per point of `object`'s box its preview `bytes` have. fn preview_density(object: &PlacedDocument, bytes: &[u8]) -> f64 { vectorcraft_doc::placed_document::image_size(bytes).map_or(0.0, |(w, _)| w as f64 / object.width.max(1e-9)) } thread_local! { /// How deep inside placed documents this thread is drawing. static DEPTH: Cell = const { Cell::new(0) }; } /// While alive: drawing inside a placed document, one level deeper. struct Inside; impl Inside { fn enter() -> Self { DEPTH.with(|d| d.set(d.get() + 1)); Inside } } impl Drop for Inside { fn drop(&mut self) { DEPTH.with(|d| d.set(d.get().saturating_sub(1))); } } fn depth() -> usize { DEPTH.with(Cell::get) } /// Render `art` of a placed document's file (`doc`, its resources) into `w`×`h` transparent /// pixels through `view`, the placed documents inside it one level deeper. pub fn render_inside(r: &mut Renderer, doc: &vectorcraft_doc::Document, art: &Arc, w: u16, h: u16, view: Affine) -> Option { let _inside = Inside::enter(); r.render_node(doc, art, w, h, view) } /// `object`'s file (`blob`) as a bitmap of `step` device pixels per point of its box: from its /// preview when that is all there is and is fine enough, else from the file (read again when /// needed; the preview when it can't be). fn make(r: &mut Renderer, object: &PlacedDocument, blob: &ImageBlob, step: f64) -> Option { let (w, h) = (pixels(object.width * step), pixels(object.height * step)); let preview = if blob.is_proxy() { Some(blob.bytes.clone()) } else { blob.proxy.clone() }; let full = match blob.is_proxy() { true if step <= preview_density(object, &blob.bytes) * 1.01 => None, true => vectorcraft_doc::placed_document::full_bytes(object, blob), false => Some(blob.bytes.clone()), }; let read = full.and_then(|bytes| vectorcraft_doc::placed_document::read(object, &bytes)); let Some(e) = read else { return crate::paint::decode_pixmap_fit(&preview?, w, h) }; let f = e.frame; let (sx, sy) = (w as f64 / f.width(), h as f64 / f.height()); let view = Affine::scale_non_uniform(sx, sy) * Affine::translate((-f.x0, -f.y0)); let img = render_inside(r, &e.doc, &e.art, w, h, view)?; let data = img.pixels.as_chunks::<4>().0.iter().map(|&[r, g, b, a]| vello_cpu::color::PremulRgba8 { r, g, b, a }).collect(); Some(Pixmap::from_parts(data, w, h)) } /// A bitmap side for `v` pixels (at least 1). fn pixels(v: f64) -> u16 { v.round().clamp(1.0, MAX_SIDE) as u16 } /// Make `job`'s bitmap and keep it. fn run(r: &mut Renderer, job: Job) { if let Some(pm) = make(r, &job.object, &job.blob, job.step) { lock(&BITMAPS).insert(job.key.clone(), pm, job.step); } lock(&PENDING).remove(&job.key); GENERATION.fetch_add(1, Ordering::Relaxed); } #[cfg(not(target_arch = "wasm32"))] mod workers { use std::sync::mpsc::{Sender, channel}; use std::sync::{Arc, LazyLock, Mutex}; use super::Job; /// The queue the workers take jobs from; `None` when no worker could start. static QUEUE: LazyLock>>>> = LazyLock::new(|| { let (tx, rx) = channel::>(); let rx = Arc::new(Mutex::new(rx)); let n = std::thread::available_parallelism().map_or(2, |n| (n.get() / 2).clamp(1, 4)); let mut started = 0; for i in 0..n { let rx = rx.clone(); let spawned = std::thread::Builder::new().name(format!("vectorcraft-placed-{i}")).spawn(move || { let mut r = crate::Renderer::new(); r.threads = 0; loop { let job = super::lock(&rx).recv(); let Ok(job) = job else { break }; // A panic loses this bitmap, not the worker. let key = job.key.clone(); if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| super::run(&mut r, *job))).is_err() { super::lock(&super::PENDING).remove(&key); } } }); started += usize::from(spawned.is_ok()); } (started > 0).then(|| Mutex::new(tx)) }); /// Hand `job` to a worker; back when there is none. pub(super) fn submit(job: Box) -> Result<(), Box> { match QUEUE.as_ref() { Some(q) => super::lock(q).send(job).map_err(|e| e.0), None => Err(job), } } } /// Make `job`'s bitmap in the background (here and now where there are no threads). fn schedule(job: Job) { if !lock(&PENDING).insert(job.key.clone()) { return; } #[cfg(not(target_arch = "wasm32"))] let job = match workers::submit(Box::new(job)) { Ok(()) => return, Err(job) => *job, }; run(&mut Renderer::new(), job); } /// The mean scale of `a`. fn mean_scale(a: Affine) -> f64 { a.determinant().abs().sqrt() } impl Renderer { /// Draw placed document `p` in frame `f`: from a cached bitmap on screen, else exactly. pub(crate) fn draw_placed(&mut self, ctx: &mut RenderContext, f: &Frame, p: &PlacedDocument) { let plain = f.ink == Ink::Display && !f.opts.outline && f.opts.proof.is_none() && !f.opts.overprint_preview; if f.opts.progressive_placed && plain && depth() == 0 && self.draw_placed_bitmap(ctx, f, p) { return; } let Some(blob) = f.doc.images.get(&p.key) else { return }; // The file's bytes: stored, read again already, or (inside another placed document, while // not too deep) read again now. let full = match depth() { 0 => vectorcraft_doc::placed_document::full_bytes_cached(p, blob), d if d < MAX_DEPTH => vectorcraft_doc::placed_document::full_bytes(p, blob), _ => None, }; let read = full.and_then(|bytes| vectorcraft_doc::placed_document::read(p, &bytes)); let Some(e) = read else { // Only the preview: drawn scaled into the box. if blob.is_proxy() && let Some(im) = p.preview_image(&blob.bytes) { self.draw_image(ctx, f, &im); } return; }; let Some(m) = p.art_xf(e.frame) else { return }; let k = mean_scale(m); if !k.is_finite() || k <= 1e-12 { return; } // The art in its own coordinates, with its own resources. let inner = Frame { doc: &e.doc, view: f.view * m, visible: m.inverse().transform_rect_bbox(f.visible), px: f.px / k, ..*f }; let _inside = Inside::enter(); self.draw_arc(ctx, &inner, &e.art); } /// Draw `p` from its bitmap at this zoom step, or (while that is made) another step's, or a /// placeholder. False when bitmaps don't suit (too large). fn draw_placed_bitmap(&mut self, ctx: &mut RenderContext, f: &Frame, p: &PlacedDocument) -> bool { let m = f.view * p.xf; let Some(step) = step(mean_scale(m)) else { return false }; if p.width * step > MAX_SIDE || p.height * step > MAX_SIDE { return false; } let Some(blob) = f.doc.images.get(&p.key).cloned() else { return true }; let key = (p.key.clone(), step.to_bits()); let (found, nearest) = { let mut b = lock(&BITMAPS); let found = b.get(&key); let nearest = if found.is_none() { b.nearest(&p.key, step) } else { None }; (found, nearest) }; let shown = match found { Some(pm) => Some(pm), None => { schedule(Job { key, object: p.clone(), blob, step }); // Made here and now (no threads) or meanwhile another step's. let mut b = lock(&BITMAPS); b.get(&(p.key.clone(), step.to_bits())).or(nearest.map(|n| n.0)) } }; let rect = Rect::new(0.0, 0.0, p.width, p.height); match shown { Some(pm) => { let (sx, sy) = (p.width / pm.width().max(1) as f64, p.height / pm.height().max(1) as f64); ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler: peniko::ImageSampler::default() }); ctx.set_transform(m); ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy)); ctx.fill_rect(&kurbo::Rect::new(rect.x0, rect.y0, rect.x1, rect.y1)); ctx.reset_paint_transform(); } None => { // Coming: a light box where it will be. ctx.set_transform(m); ctx.set_paint(peniko::Color::from_rgba8(128, 128, 128, 40)); ctx.fill_rect(&kurbo::Rect::new(rect.x0, rect.y0, rect.x1, rect.y1)); } } true } } #[cfg(test)] mod tests { use super::*; #[test] fn zoom_steps_are_powers_of_two_at_or_above_the_scale() { assert_eq!(step(1.0), Some(1.0)); assert_eq!(step(1.2), Some(2.0)); assert_eq!(step(0.3), Some(0.5)); assert_eq!(step(5.0), Some(8.0)); assert_eq!(step(1e-9), Some(MIN_STEP)); assert_eq!(step(f64::NAN), None); assert_eq!(step(0.0), None); } }