diff --git a/crates/images/src/eps.rs b/crates/images/src/eps.rs index bd2498f1a..1f209ccf0 100644 --- a/crates/images/src/eps.rs +++ b/crates/images/src/eps.rs @@ -98,7 +98,7 @@ pub fn eps_size(bytes: &[u8]) -> Option<(f64, f64)> { ((0.001..=20_000.0).contains(&w) && (0.001..=20_000.0).contains(&h)).then_some((w, h)) } -fn preview(bytes: &[u8]) -> Option<(&[u8], (u32, u32))> { +pub(crate) fn preview(bytes: &[u8]) -> Option<(&[u8], (u32, u32))> { let (_, tiff) = sections(bytes); let tiff = tiff?; let (w, h) = crate::pixel_size(tiff)?; diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index 081af6c2b..820f9a8b4 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -442,6 +442,39 @@ pub fn decode_rgba(bytes: &[u8]) -> Option { to_display_rgba(px, Some(&icc), &DisplayColour::active()) } +/// About how many bytes a pixel [`decode_rgba`] of `bytes` holds at once, with a premultiplied +/// copy of its result: the samples as stored beside the copy they are converted to, and at least +/// 8 (the RGBA result and its premultiplied copy). 16-bit CMYK TIFF samples are narrowed into an +/// 8-bit copy and CMYK alpha is split off into one, so a 16-bit CMYKA TIFF counts 15. An EPS +/// counts as its TIFF preview, or 8 for its placeholder or drawn PostScript. +pub fn decode_bytes_per_pixel(bytes: &[u8]) -> usize { + if is_eps(bytes) { + return eps::preview(bytes).map_or(8, |(tiff, _)| if is_eps(tiff) { 8 } else { decode_bytes_per_pixel(tiff) }); + } + if is_psd(bytes) { + // RGBA-sized channels, an alpha plane, and the RGBA copy of CMYK inks. + return 9; + } + if let Some(mut d) = tiff_decoder(bytes) { + let cmyk = match d.colortype() { + Ok(tiff::ColorType::CMYK(bits)) => Some((4, bits)), + Ok(tiff::ColorType::CMYKA(bits)) => Some((5, bits)), + _ => None, + }; + if let Some((samples, bits)) = cmyk { + let stored = samples * usize::from(bits).div_ceil(8); + let copy = if bits > 8 || samples == 5 { samples } else { 0 }; + return (stored + copy).max(samples + 4).max(8); + } + } + let sample = image::ImageReader::new(std::io::Cursor::new(bytes)) + .with_guessed_format() + .ok() + .and_then(|r| r.into_decoder().ok()) + .map_or(4, |d| usize::from(image::ImageDecoder::color_type(&d).bytes_per_pixel())); + (sample + 4).max(8) +} + /// False when `bytes` is a WebP whose `ICCP` chunk declares more bytes than the file holds or than /// [`colour::MAX_ICC_BYTES`]. The decoder sizes its buffer from the declared length before it reads /// anything, so such a file must not reach `icc_profile()`; its pixels decode without it. diff --git a/crates/images/src/tests.rs b/crates/images/src/tests.rs index cd577f09a..0d8e65ab2 100644 --- a/crates/images/src/tests.rs +++ b/crates/images/src/tests.rs @@ -372,6 +372,32 @@ fn cmyk_tiff_keeps_inks() { assert_eq!(decode_cmyk_tiff(&b[..b.len() / 2]), None, "truncated"); } +/// A decode holds the samples as stored beside the copy they are converted to. +#[test] +fn decode_bytes_per_pixel_counts_the_stored_samples_and_their_copy() { + use tiff::encoder::colortype::{CMYK8, CMYK16, CMYKA8, RGB16}; + assert_eq!(decode_bytes_per_pixel(&tiff_of::(1, 1, &[0; 4], false)), 8); + assert_eq!(decode_bytes_per_pixel(&tiff_of::(1, 1, &[0; 5], true)), 10); + assert_eq!(decode_bytes_per_pixel(&tiff_of::(1, 1, &[0; 4], false)), 12); + assert_eq!(decode_bytes_per_pixel(&tiff_of::(1, 1, &[0; 3], false)), 10); + let mut psd = b"8BPS".to_vec(); + psd.resize(26, 0); + assert_eq!(decode_bytes_per_pixel(&psd), 9); + assert_eq!(decode_bytes_per_pixel(b"not an image"), 8); + // An EPS counts as its TIFF preview, or 8 without one. + let ps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 10 10\nnewpath\n%%EOF\n"; + assert_eq!(decode_bytes_per_pixel(ps), 8); + let preview = tiff_of::(1, 1, &[0; 4], false); + let mut dos = vec![0xC5, 0xD0, 0xD3, 0xC6]; + for v in [30, ps.len(), 0, 0, 30 + ps.len(), preview.len()] { + dos.extend((v as u32).to_le_bytes()); + } + dos.extend([0xFF, 0xFF]); + dos.extend_from_slice(ps); + dos.extend_from_slice(&preview); + assert_eq!(decode_bytes_per_pixel(&dos), 12); +} + /// Indexed colour (PhotometricInterpretation 3), which the `tiff` encoder has no type for. struct Palette; impl tiff::encoder::colortype::ColorType for Palette { diff --git a/crates/render/src/decode.rs b/crates/render/src/decode.rs index 5668a205f..dfaed5c55 100644 --- a/crates/render/src/decode.rs +++ b/crates/render/src/decode.rs @@ -12,18 +12,39 @@ const VECTOR_SIDE: u32 = 3000; /// About how many bytes [`decode_pixmap_page`] of `bytes` takes, with its smaller levels (a third /// more): PDF pages and SVGs at [`VECTOR_SIDE`] pixels on the longer side, an EPS without a /// preview at up to 8 pixels per point and 4000 pixels on the longer side (as its PostScript is -/// drawn), other images and previews at their pixel size. 0 when the size can't be read. +/// drawn) or at its placeholder's size when that is larger, other images and previews at their +/// pixel size. 0 when the size can't be read. +#[cfg(test)] pub(crate) fn decoded_bytes(bytes: &[u8], page: u32) -> usize { + decode_sizes(bytes, page).0 +} + +/// [`decoded_bytes`] of `bytes`, and about how many bytes [`decode_pixmap_page`] of it holds at +/// once while it runs: a raster as decoded and its RGBA copy, or that copy and the premultiplied +/// pixels, whichever is more ([`designcraft_images::decode_bytes_per_pixel`]: 8 bytes a pixel +/// for 8-bit color, 12 for 16-bit RGBA, an EPS as its TIFF preview). PDF pages, SVGs and EPS +/// files without a preview count 8 bytes a pixel. Both are 0 when the size can't be read. +pub(crate) fn decode_sizes(bytes: &[u8], page: u32) -> (usize, usize) { + let Some((w, h)) = drawn_size(bytes, page) else { return (0, 0) }; + let raster = !is_pdf(bytes) && !designcraft_images::is_svg(bytes); + let per_pixel = if raster { designcraft_images::decode_bytes_per_pixel(bytes) } else { 8 } as f64; + ((w * h * 16.0 / 3.0) as usize, (w * h * per_pixel) as usize) +} + +/// The pixel size [`decode_pixmap_page`] of `bytes` draws at, as [`decoded_bytes`] describes it. +fn drawn_size(bytes: &[u8], page: u32) -> Option<(f64, f64)> { let drawn = |(w, h): (f64, f64), side: f64, most: f64| { let k = (side / w.max(h).max(1e-3)).min(most); ((w * k).round().max(1.0), (h * k).round().max(1.0)) }; - let size = if designcraft_images::is_eps(bytes) { + if designcraft_images::is_eps(bytes) { image_size(bytes).zip(designcraft_images::eps_size(bytes)).map(|((pw, ph), (w, h))| { - // Without a preview, the pixel size is the 2 pixels per point placeholder's. + // Without a preview, the pixel size is the 2 pixels per point placeholder's. The + // PostScript may be drawn instead, at another size: count the larger of the two. let placeholder = ((w * 2.0).round().clamp(2.0, 4000.0), (h * 2.0).round().clamp(2.0, 4000.0)); let px = (pw as f64, ph as f64); - if px == placeholder { drawn((w, h), 4000.0, 8.0) } else { px } + let rendered = drawn((w, h), 4000.0, 8.0); + if px == placeholder && rendered.0 * rendered.1 > px.0 * px.1 { rendered } else { px } }) } else if is_pdf(bytes) { // As render_pdf_page scales: at most 8 times the page size. @@ -32,8 +53,7 @@ pub(crate) fn decoded_bytes(bytes: &[u8], page: u32) -> usize { designcraft_images::natural_size(bytes).map(|s| drawn(s, VECTOR_SIDE as f64, f64::INFINITY)) } else { image_size(bytes).map(|(w, h)| (w as f64, h as f64)) - }; - size.map_or(0, |(w, h)| (w * h * 16.0 / 3.0) as usize) + } } /// [`decode_pixmap`] for page `page` of a PDF (other formats have one page). diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index ee0d917ba..dca7bcead 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -11,7 +11,7 @@ use std::sync::{Arc, Condvar, Mutex}; use vello_cpu::Pixmap; -use crate::decode::decoded_bytes; +use crate::decode::decode_sizes; use crate::{decode_pixmap_page, halve}; /// Decoded pixels kept (bytes). @@ -190,18 +190,20 @@ fn admits(in_flight: usize, size: usize) -> bool { in_flight == 0 || in_flight.saturating_add(size) <= BUDGET / 2 } -/// Bytes decoding at once and the most there were, with the signal that some finished. -type Gate = (Mutex<(usize, usize)>, Condvar); +/// Bytes [`warm`] is decoding at once in every render in the process (canvas worker, thumbnails, +/// exports), with the signal that some finished. +static GATE: (Mutex, Condvar) = (Mutex::new(0), Condvar::new()); -/// Takes a finished decode's bytes off its [`Gate`] when dropped. -struct Release<'a>(&'a Gate, usize); +/// Takes a finished decode's bytes off [`GATE`] and off its own call's count when dropped. +struct Release<'a>(&'a AtomicUsize, usize); impl Drop for Release<'_> { fn drop(&mut self) { - let mut g = self.0.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - g.0 = g.0.saturating_sub(self.1); + self.0.fetch_sub(self.1, Ordering::Relaxed); + let mut g = GATE.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + *g = g.saturating_sub(self.1); drop(g); - self.0.1.notify_all(); + GATE.1.notify_all(); } } @@ -210,8 +212,8 @@ impl Drop for Release<'_> { /// of decoding them one after another. Does nothing when fewer than two images need decoding. /// The images in view that are cached count against the cache budget first; a new image that /// would take the total past what the cache keeps (by its pixel size) is left to the render. -/// Decodes wait while those in flight together would take more than half the budget. Returns -/// the most bytes (estimated) that were decoding at once. +/// Decodes wait while those in flight in every render together would take more than half the +/// budget. Returns the most bytes (estimated) this call had decoding at once. pub(crate) fn warm(wants: Vec) -> usize { let workers = workers(); if workers < 2 { @@ -263,7 +265,7 @@ pub(crate) fn warm(wants: Vec) -> usize { if held.contains_key(&key) { continue; } - let size = wanted.first().map_or(0, |w| decoded_bytes(&w.bytes, w.page)); + let (size, decoding) = wanted.first().map_or((0, 0), |w| decode_sizes(&w.bytes, w.page)); // A store past the budget evicts down to three quarters of it: the images in view must // fit in that, unless they and the rest fit in the budget. let total = pixels.saturating_add(size); @@ -271,29 +273,34 @@ pub(crate) fn warm(wants: Vec) -> usize { continue; } pixels = total; - jobs.push((size, wanted)); + jobs.push((decoding, wanted)); } if jobs.len() < 2 { return 0; } - // While it converts the pixels, a decode holds buffers of about its size besides the cache: - // a decode waits while those in flight would take it past half the budget (one always runs). - let gate: Gate = (Mutex::new((0, 0)), Condvar::new()); - let lock = || gate.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + // While it converts the pixels, a decode holds the buffers `decode_sizes` counts: a + // decode waits while those in flight in any render would take it past half the budget (one + // always runs). + let lock = || GATE.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let next = AtomicUsize::new(0); + // This call's bytes decoding, and the most there were. + let (mine, peak) = (AtomicUsize::new(0), AtomicUsize::new(0)); std::thread::scope(|s| { for _ in 0..workers.min(jobs.len()) { let work = || { while let Some((size, job)) = jobs.get(next.fetch_add(1, Ordering::Relaxed)) { let mut g = lock(); - while !admits(g.0, *size) { - g = gate.1.wait(g).unwrap_or_else(std::sync::PoisonError::into_inner); + while !admits(*g, *size) { + #[cfg(test)] + waited(job); + g = GATE.1.wait(g).unwrap_or_else(std::sync::PoisonError::into_inner); } - g.0 += size; - g.1 = g.1.max(g.0); + *g = g.saturating_add(*size); + let now = mine.fetch_add(*size, Ordering::Relaxed).saturating_add(*size); + peak.fetch_max(now, Ordering::Relaxed); drop(g); // Released even if the decode panics, so no other decode waits forever. - let _release = Release(&gate, *size); + let _release = Release(&mine, *size); for w in job { let _ = mip(&w.bytes, w.page, w.width_pt, w.on_screen); } @@ -305,7 +312,19 @@ pub(crate) fn warm(wants: Vec) -> usize { } } }); - gate.0.into_inner().unwrap_or_else(std::sync::PoisonError::into_inner).1 + peak.into_inner() +} + +/// The images whose decode waited at [`GATE`] (the address of the encoded bytes), with the +/// signal that one did. +#[cfg(test)] +static WAITED: (Mutex>, Condvar) = (Mutex::new(Vec::new()), Condvar::new()); + +#[cfg(test)] +fn waited(job: &[Want]) { + let id = job.first().map_or(0, |w| Arc::as_ptr(&w.bytes) as usize); + WAITED.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner).push(id); + WAITED.1.notify_all(); } /// Drop every decoded image. @@ -320,6 +339,11 @@ pub fn clear() { #[cfg(test)] mod tests { use super::*; + use crate::decode::decoded_bytes; + + fn decode_peak_bytes(bytes: &[u8], page: u32) -> usize { + decode_sizes(bytes, page).1 + } #[test] fn failed_assets_are_capped_and_oldest_is_dropped() { @@ -403,21 +427,74 @@ mod tests { #[test] fn warm_decodes_large_images_one_at_a_time() { - // Two 7,000 pixel square images fit the budget together, but decoding both at once would + // Two 6,500 pixel square images fit the budget together, but decoding both at once would // take more than half of it; the small image still decodes beside them. - let (d, e, small) = (psd(7000), psd(7000), png(8, 15)); + let (d, e, small) = (psd(6500), psd(6500), png(8, 15)); let since = mark(); let large = decoded_bytes(&d, 0); assert!(large > BUDGET / 4 && 2 * large <= BUDGET * 3 / 4); - assert!(admits(0, large) && !admits(large, large) && admits(large, decoded_bytes(&small, 0))); + let decoding = decode_peak_bytes(&d, 0); + assert!(admits(0, decoding) && !admits(decoding, decoding) && admits(decoding, decode_peak_bytes(&small, 0))); assert!(admits(0, usize::MAX), "an image over half the budget still decodes alone"); let peak = warm(vec![want(&d), want(&e), want(&small)]); assert!(peak <= BUDGET / 2, "{peak} bytes decoding at once"); - assert!(peak >= large, "the large images were decoded ahead"); + assert!(peak >= decoding, "the large images were decoded ahead"); let me = std::thread::current().id(); for img in [&d, &e, &small] { let on = decoders(img, since); assert!(on.len() == 1 && on[0] != me); } } + + #[test] + fn warm_counts_a_decode_at_its_peak() { + // A decode holds the RGBA copy beside the decoded or the premultiplied pixels: two 5,500 + // pixel square images would take less than half the budget cached, but more than half of + // it while both decode, so they decode one at a time. + let (f, g) = (psd(5500), psd(5500)); + let decoding = decode_peak_bytes(&f, 0); + assert_eq!(decoding, 5500 * 5500 * 9, "9 bytes a pixel for a PSD"); + assert!(2 * decoded_bytes(&f, 0) <= BUDGET / 2 && !admits(decoding, decode_peak_bytes(&g, 0))); + assert_eq!(warm(vec![want(&f), want(&g)]), decoding, "one decode at a time"); + // 16-bit RGBA decodes at 8 bytes a pixel before its RGBA copy. + let mut deep = Vec::new(); + let img = image::ImageBuffer::, Vec>::from_pixel(4, 4, image::Rgba([9, 0, 0, 65535])); + img.write_to(&mut std::io::Cursor::new(&mut deep), image::ImageFormat::Png).unwrap(); + assert_eq!(decode_peak_bytes(&deep, 0), 16 * 12); + assert_eq!(decode_peak_bytes(&png(8, 1), 0), 64 * 8); + } + + #[test] + fn warm_waits_for_decodes_in_other_renders() { + // Another render's decodes take half the budget: this render's decodes wait for them. + let (a, b) = (png(8, 17), png(8, 19)); + let ids = [Arc::as_ptr(&a) as usize, Arc::as_ptr(&b) as usize]; + let (since, decoded_since) = (WAITED.0.lock().unwrap().len(), mark()); + let other = BUDGET / 2; + *GATE.0.lock().unwrap() += other; + let done = std::sync::atomic::AtomicBool::new(false); + let (waited, peak) = std::thread::scope(|s| { + let render = s.spawn(|| { + let peak = warm(vec![want(&a), want(&b)]); + let w = WAITED.0.lock().unwrap(); + done.store(true, Ordering::SeqCst); + drop(w); + WAITED.1.notify_all(); + peak + }); + let seen = |w: &Vec| w.iter().skip(since).any(|id| ids.contains(id)); + let mut w = WAITED.0.lock().unwrap(); + while !seen(&w) && !done.load(Ordering::SeqCst) { + w = WAITED.1.wait(w).unwrap(); + } + let waited = seen(&w); + drop(w); + *GATE.0.lock().unwrap() -= other; + GATE.1.notify_all(); + (waited, render.join().unwrap()) + }); + assert!(waited, "decoded beside the other render's decodes"); + assert_eq!((decoders(&a, decoded_since).len(), decoders(&b, decoded_since).len()), (1, 1)); + assert!(peak > 0 && peak <= BUDGET / 2, "{peak} bytes decoding at once"); + } } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 5d4f6a048..82e8fc77f 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -792,6 +792,14 @@ fn decode_size_estimates_match_the_decodes() { let drawn = pm.width() as usize * pm.height() as usize * 16 / 3; assert!(drawn > 200 * 100 * 16 / 3, "drawn above 2 pixels per point: {}×{}", pm.width(), pm.height()); assert!(decode::decoded_bytes(eps, 0) >= drawn); + // A wide EPS whose PostScript isn't drawn keeps its placeholder, clamped to 4000 pixels on + // each side: it is budgeted at no less than that. + let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 10000 4000\n%%EndComments\n0 0 moveto 10 10 lineto stroke\n%%EOF\n"; + let pm = decode_pixmap_page(eps, 0).unwrap(); + assert_eq!((pm.width(), pm.height()), (4000, 4000)); + let px = pm.width() as usize * pm.height() as usize; + assert!(decode::decoded_bytes(eps, 0) >= px * 16 / 3); + assert!(decode::decode_sizes(eps, 0).1 >= px * 8); } #[test]