mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Images: harden the background decodes
- A panic anywhere in a background job, sizing and the wait at the gate included, marks the image bad. A guard takes the job off the busy list however it ends, and a thread that ends is counted out so another starts. - Background decodes reserve their bytes where mip chooses the decode: the reduced JPEG estimate before a reduced decode (a JPEG without one goes to the full decode), the full peak before a full decode, nothing when a cached level is halved. - A request turned away as recently decoded schedules one generation advance for when its window has passed, so its stand-in doesn't stay until the view changes. Emptying the cache (new colour settings, clear) starts a new epoch, and decodes into the old cache no longer count as recent. - At most two full decodes are kept for zooming in; a newer one evicts the oldest. - warm's planning runs under catch_unwind; a panic skips warming. - Renderer::images_pending says whether the last render drew an image still decoding. Regression tests use a test-only fault hook that panics inside the job and inside warm's planning.
This commit is contained in:
1 parent
eefdfbb688
commit
0a97679647
3 files changed
+301
-76
No files matched your search
@@ -17,6 +17,7 @@ impl Renderer {
|
||||
drawing_items: Vec::new(),
|
||||
cmyk_view: None,
|
||||
cmyk_ran: false,
|
||||
images_pending: false,
|
||||
kept: Default::default(),
|
||||
#[cfg(test)]
|
||||
cmyk_passes: 0,
|
||||
@@ -39,6 +40,12 @@ impl Renderer {
|
||||
self.kept.clear();
|
||||
}
|
||||
|
||||
/// Whether the last render drew an image still decoding in the background in its place
|
||||
/// ([`RenderOptions::defer_images`]): render again when [`images::generation`] advances.
|
||||
pub fn images_pending(&self) -> bool {
|
||||
self.images_pending
|
||||
}
|
||||
|
||||
/// Number of text frames whose glyph paths are cached.
|
||||
pub fn cached_frames(&self) -> usize {
|
||||
self.glyphs.len()
|
||||
@@ -51,6 +58,8 @@ impl Renderer {
|
||||
RICH_BLACK.with(|r| r.set(opts.rich_black));
|
||||
PLATE.with(|p| p.set(opts.plate));
|
||||
self.kept.begin(doc);
|
||||
// A kept view had every image drawn.
|
||||
self.images_pending = false;
|
||||
if let Some(hit) = self.cmyk_view.as_ref().filter(|entry| entry.matches(doc, spreads, width, height, view, opts, self.threads)) {
|
||||
let image = hit.image();
|
||||
self.stats = hit.stats();
|
||||
@@ -65,6 +74,7 @@ impl Renderer {
|
||||
// Kept only when the ink passes ran: other views render at their usual cost anyway. Not
|
||||
// with images still decoding: the next render must show them.
|
||||
let deferred = images::take_deferred();
|
||||
self.images_pending = deferred;
|
||||
if self.cmyk_ran && !deferred && image.pixels.len() <= blendspace::CachedView::MAX_BYTES {
|
||||
self.cmyk_view = Some(blendspace::CachedView::new(doc, spreads, view, opts, &image, self.stats, self.threads));
|
||||
}
|
||||
|
||||
+289
-76
@@ -33,6 +33,10 @@ const MAX_LEVEL: u8 = 8;
|
||||
/// from it instead of decoded again.
|
||||
const KEEP_FULL: usize = BUDGET / 8;
|
||||
|
||||
/// Full decodes kept for zooming in at once: an older one is evicted for a newer one, so a
|
||||
/// spread of photos each with a coarser level cached doesn't fill the cache with full decodes.
|
||||
const MAX_KEPT_FULL: usize = 2;
|
||||
|
||||
struct Entry {
|
||||
_bytes: Arc<Vec<u8>>,
|
||||
pm: Arc<Pixmap>,
|
||||
@@ -54,6 +58,11 @@ struct Cache {
|
||||
dims: HashMap<(usize, u32), (Weak<Vec<u8>>, (u32, u32))>,
|
||||
/// The colour settings the pixels were converted with ([`designcraft_color::cms::generation`]).
|
||||
colour: u64,
|
||||
/// Advances each time the cache is emptied, so records of decodes into the old cache
|
||||
/// (the background queue's recent decodes) no longer count.
|
||||
epoch: u64,
|
||||
/// Full decodes kept for a zoom-in ([`KEEP_FULL`]), oldest first; at most [`MAX_KEPT_FULL`].
|
||||
kept_full: std::collections::VecDeque<(usize, u32)>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
@@ -80,7 +89,7 @@ fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R {
|
||||
// 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() };
|
||||
*c = Cache { clock: c.clock, colour, epoch: c.epoch.wrapping_add(1), ..Cache::default() };
|
||||
}
|
||||
f(c)
|
||||
}
|
||||
@@ -101,6 +110,12 @@ fn is_cached(key: (usize, u32, u8)) -> bool {
|
||||
with(|c| c.map.contains_key(&key))
|
||||
}
|
||||
|
||||
/// The cache's [`Cache::epoch`].
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn epoch() -> u64 {
|
||||
with(|c| c.epoch)
|
||||
}
|
||||
|
||||
fn is_bad(id: usize, page: u32) -> bool {
|
||||
with(|c| c.bad.contains_key(&(id, page)))
|
||||
}
|
||||
@@ -186,6 +201,21 @@ fn log_decode(id: usize) {
|
||||
#[cfg(not(test))]
|
||||
fn log_decode(_: usize) {}
|
||||
|
||||
/// Images whose background decode or warm planning panics (tests: a decoder that panics).
|
||||
#[cfg(test)]
|
||||
static FAULTS: Mutex<Vec<usize>> = Mutex::new(Vec::new());
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::panic)]
|
||||
fn fault(id: usize) {
|
||||
if FAULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner).contains(&id) {
|
||||
panic!("test fault");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn fault(_: usize) {}
|
||||
|
||||
/// 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;
|
||||
@@ -242,16 +272,16 @@ fn cached_level(bytes: &Arc<Vec<u8>>, page: u32, level: u8) -> Option<Arc<Pixmap
|
||||
Some(pm)
|
||||
}
|
||||
|
||||
/// Whether level `level` of an asset, or a finer one it is halved from, is cached.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn has_level(key: (usize, u32), level: u8) -> bool {
|
||||
with(|c| (0..=level).any(|l| c.map.contains_key(&(key.0, key.1, l))))
|
||||
}
|
||||
|
||||
/// The mip level of `bytes` closest above `on_screen` device pixels per point for a graphic
|
||||
/// `width_pt` points wide. Only that level is cached, not the full decode it is built from,
|
||||
/// except while an image is zoomed into (see [`KEEP_FULL`]).
|
||||
pub fn mip(bytes: &Arc<Vec<u8>>, page: u32, width_pt: f64, on_screen: f64) -> Option<Arc<Pixmap>> {
|
||||
mip_gated(bytes, page, width_pt, on_screen, false)
|
||||
}
|
||||
|
||||
/// [`mip`]; with `gate`, a decode first waits at [`GATE`] for its peak bytes ([`reserve`]),
|
||||
/// counted for the decode path it takes.
|
||||
fn mip_gated(bytes: &Arc<Vec<u8>>, page: u32, width_pt: f64, on_screen: f64, gate: bool) -> Option<Arc<Pixmap>> {
|
||||
let id = Arc::as_ptr(bytes) as usize;
|
||||
if is_bad(id, page) {
|
||||
return None;
|
||||
@@ -262,15 +292,21 @@ pub fn mip(bytes: &Arc<Vec<u8>>, page: u32, width_pt: f64, on_screen: f64) -> Op
|
||||
if let Some(pm) = cached_level(bytes, page, level) {
|
||||
return Some(pm);
|
||||
}
|
||||
if let Some(pm) = reduced(bytes, page, level) {
|
||||
if let Some(pm) = reduced(bytes, page, level, gate) {
|
||||
return Some(pm);
|
||||
}
|
||||
zoom_in = (level + 1..=MAX_LEVEL).any(|l| is_cached((id, page, l)));
|
||||
}
|
||||
let _reserved = gate.then(|| reserve(decode_sizes(bytes, page).1.saturating_mul(3) / 2, id, None));
|
||||
let full = decode_full(bytes, page)?;
|
||||
let level = level_for((u32::from(full.width()), u32::from(full.height())), width_pt, on_screen);
|
||||
if level == 0 || (zoom_in && size_of(&full) <= KEEP_FULL) {
|
||||
if level == 0 {
|
||||
store(bytes, page, 0, full.clone());
|
||||
// Drawn, not kept for a zoom-in: not evicted for another one.
|
||||
with(|c| c.kept_full.retain(|k| *k != (id, page)));
|
||||
} else if zoom_in && size_of(&full) <= KEEP_FULL {
|
||||
store(bytes, page, 0, full.clone());
|
||||
keep_full((id, page));
|
||||
}
|
||||
let pm = halve_to(full, 0, level);
|
||||
if level > 0 {
|
||||
@@ -279,6 +315,21 @@ pub fn mip(bytes: &Arc<Vec<u8>>, page: u32, width_pt: f64, on_screen: f64) -> Op
|
||||
Some(pm)
|
||||
}
|
||||
|
||||
/// Count the full decode of `key` just stored as kept for a zoom-in, evicting the oldest such
|
||||
/// decodes beyond [`MAX_KEPT_FULL`].
|
||||
fn keep_full(key: (usize, u32)) {
|
||||
with(|c| {
|
||||
c.kept_full.retain(|k| *k != key);
|
||||
c.kept_full.push_back(key);
|
||||
while c.kept_full.len() > MAX_KEPT_FULL {
|
||||
let Some(old) = c.kept_full.pop_front() else { break };
|
||||
if let Some(e) = c.map.remove(&(old.0, old.1, 0)) {
|
||||
c.bytes = c.bytes.saturating_sub(size_of(&e.pm));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// What [`mip_now`] has to draw.
|
||||
pub enum Mip {
|
||||
/// The level asked for.
|
||||
@@ -344,6 +395,7 @@ mod background {
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Condvar, Mutex, PoisonError, Weak};
|
||||
use std::time::Instant;
|
||||
|
||||
use super::Want;
|
||||
|
||||
@@ -379,13 +431,16 @@ mod background {
|
||||
bytes: Weak<Vec<u8>>,
|
||||
key: (usize, u32),
|
||||
level: Option<u8>,
|
||||
at: std::time::Instant,
|
||||
at: Instant,
|
||||
/// The cache's epoch then: a record of a decode into a cache emptied since doesn't count.
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
impl Done {
|
||||
/// Whether this is `key` at `level`, finished too recently to decode in the background again.
|
||||
fn is_recent(&self, key: (usize, u32), level: Option<u8>) -> bool {
|
||||
self.key == key && self.level == level && self.bytes.strong_count() > 0 && self.at.elapsed() < RECENT
|
||||
/// Whether this is `key` at `level`, finished too recently to decode in the background
|
||||
/// again (into the cache of `epoch`).
|
||||
fn is_recent(&self, key: (usize, u32), level: Option<u8>, epoch: u64) -> bool {
|
||||
self.key == key && self.level == level && self.epoch == epoch && self.bytes.strong_count() > 0 && self.at.elapsed() < RECENT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,16 +451,24 @@ mod background {
|
||||
/// The last decodes finished, newest last.
|
||||
done: VecDeque<Done>,
|
||||
threads: usize,
|
||||
/// When to advance the generation once more: a request turned away as [`Queued::Recent`]
|
||||
/// is drawn when its window has passed.
|
||||
wake_at: Option<Instant>,
|
||||
}
|
||||
|
||||
static QUEUE: Mutex<Queue> = Mutex::new(Queue { jobs: VecDeque::new(), busy: Vec::new(), done: VecDeque::new(), threads: 0 });
|
||||
static QUEUE: Mutex<Queue> = Mutex::new(Queue { jobs: VecDeque::new(), busy: Vec::new(), done: VecDeque::new(), threads: 0, wake_at: None });
|
||||
static READY: Condvar = Condvar::new();
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, Queue> {
|
||||
QUEUE.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// What [`queue`] did with a request.
|
||||
pub(super) enum Queued {
|
||||
/// Queued, or already decoding.
|
||||
Yes,
|
||||
/// Decoded moments ago and evicted since: not queued again yet.
|
||||
/// Decoded moments ago and evicted since: not queued again yet (the generation advances
|
||||
/// when that may happen).
|
||||
Recent,
|
||||
/// No decode thread can run: decode in place.
|
||||
InPlace,
|
||||
@@ -415,12 +478,17 @@ mod background {
|
||||
pub(super) fn queue(want: Want, level: Option<u8>) -> Queued {
|
||||
let job = Job { want, level };
|
||||
let key = job.key();
|
||||
let mut q = QUEUE.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let epoch = super::epoch();
|
||||
let mut q = lock();
|
||||
if q.busy.contains(&key) {
|
||||
// Finishing it advances the generation; a different size is asked for then.
|
||||
return Queued::Yes;
|
||||
}
|
||||
if q.done.iter().any(|d| d.is_recent(key, level)) {
|
||||
if let Some(at) = q.done.iter().find(|d| d.is_recent(key, level, epoch)).and_then(|d| d.at.checked_add(RECENT)) {
|
||||
if q.wake_at.is_none_or(|w| at < w) {
|
||||
q.wake_at = Some(at);
|
||||
READY.notify_one();
|
||||
}
|
||||
return Queued::Recent;
|
||||
}
|
||||
q.jobs.retain(|j| j.key() != key);
|
||||
@@ -440,51 +508,104 @@ mod background {
|
||||
Queued::Yes
|
||||
}
|
||||
|
||||
fn run() {
|
||||
/// The next job, or `None` when [`Queue::wake_at`] has come.
|
||||
fn next() -> Option<Job> {
|
||||
let mut q = lock();
|
||||
loop {
|
||||
let job = {
|
||||
let mut q = QUEUE.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
loop {
|
||||
if let Some(job) = q.jobs.pop_back() {
|
||||
q.busy.push(job.key());
|
||||
break job;
|
||||
}
|
||||
q = READY.wait(q).unwrap_or_else(PoisonError::into_inner);
|
||||
if let Some(job) = q.jobs.pop_back() {
|
||||
q.busy.push(job.key());
|
||||
return Some(job);
|
||||
}
|
||||
match q.wake_at {
|
||||
Some(at) if Instant::now() >= at => {
|
||||
q.wake_at = None;
|
||||
return None;
|
||||
}
|
||||
Some(at) => {
|
||||
let wait = at.saturating_duration_since(Instant::now());
|
||||
q = READY.wait_timeout(q, wait).map_or_else(|e| e.into_inner().0, |(g, _)| g);
|
||||
}
|
||||
None => q = READY.wait(q).unwrap_or_else(PoisonError::into_inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the generation and run the waker.
|
||||
fn advance() {
|
||||
GENERATION.fetch_add(1, Ordering::AcqRel);
|
||||
let waker = WAKER.lock().unwrap_or_else(PoisonError::into_inner).clone();
|
||||
if let Some(wake) = waker {
|
||||
wake();
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a job off the busy list and records it as done when dropped, however the job ended.
|
||||
struct Busy<'a>(&'a Job);
|
||||
|
||||
impl Drop for Busy<'_> {
|
||||
fn drop(&mut self) {
|
||||
let (key, epoch) = (self.0.key(), super::epoch());
|
||||
let mut q = lock();
|
||||
q.busy.retain(|k| *k != key);
|
||||
q.done.push_back(Done { bytes: Arc::downgrade(&self.0.want.bytes), key, level: self.0.level, at: Instant::now(), epoch });
|
||||
while q.done.len() > 64 {
|
||||
q.done.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts its thread out of the decode threads when dropped (the thread ended), so a new
|
||||
/// one is started for the next request.
|
||||
struct Alive;
|
||||
|
||||
impl Drop for Alive {
|
||||
fn drop(&mut self) {
|
||||
let mut q = lock();
|
||||
q.threads = q.threads.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() {
|
||||
let _alive = Alive;
|
||||
loop {
|
||||
let Some(job) = next() else {
|
||||
advance();
|
||||
continue;
|
||||
};
|
||||
let key = job.key();
|
||||
let busy = Busy(&job);
|
||||
let w = &job.want;
|
||||
let ok = {
|
||||
let (_release, _) = super::reserve(super::decode_reservation(w), key.0, None);
|
||||
// A decoder panic must not take the thread with it.
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| super::mip(&w.bytes, w.page, w.width_pt, w.on_screen))) {
|
||||
Ok(pm) => pm.is_some(),
|
||||
Err(_) => {
|
||||
super::with(|c| c.mark_bad(key, &w.bytes));
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut q = QUEUE.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
q.busy.retain(|k| *k != key);
|
||||
q.done.push_back(Done { bytes: Arc::downgrade(&w.bytes), key, level: job.level, at: std::time::Instant::now() });
|
||||
while q.done.len() > 64 {
|
||||
q.done.pop_front();
|
||||
}
|
||||
}
|
||||
// A panic anywhere in the job (sizing, waiting, decoding) must not take the thread
|
||||
// with it, nor leave the image drawn as decoding.
|
||||
let run = std::panic::AssertUnwindSafe(|| {
|
||||
super::fault(key.0);
|
||||
super::mip_gated(&w.bytes, w.page, w.width_pt, w.on_screen, true).is_some()
|
||||
});
|
||||
let ok = std::panic::catch_unwind(run).unwrap_or_else(|_| {
|
||||
super::with(|c| c.mark_bad(key, &w.bytes));
|
||||
false
|
||||
});
|
||||
drop(busy);
|
||||
// A failure redraws the image as failed (no placeholder), unless the failures fill
|
||||
// the list of bad images: forgotten ones would be asked for again, without end.
|
||||
if ok || super::with(|c| c.bad.len() < super::MAX_BAD) {
|
||||
GENERATION.fetch_add(1, Ordering::AcqRel);
|
||||
let waker = WAKER.lock().unwrap_or_else(PoisonError::into_inner).clone();
|
||||
if let Some(wake) = waker {
|
||||
wake();
|
||||
}
|
||||
advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the image at address `id` is being decoded.
|
||||
#[cfg(test)]
|
||||
pub(super) fn is_busy(id: usize) -> bool {
|
||||
lock().busy.iter().any(|k| k.0 == id)
|
||||
}
|
||||
|
||||
/// Whether a generation advance is scheduled ([`Queue::wake_at`]).
|
||||
#[cfg(test)]
|
||||
pub(super) fn wake_scheduled() -> bool {
|
||||
lock().wake_at.is_some()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -495,12 +616,13 @@ mod background {
|
||||
fn a_finished_decode_counts_only_while_its_image_lives() {
|
||||
let bytes = Arc::new(vec![1u8]);
|
||||
let key = (Arc::as_ptr(&bytes) as usize, 0);
|
||||
let done = Done { bytes: Arc::downgrade(&bytes), key, level: Some(1), at: std::time::Instant::now() };
|
||||
assert!(done.is_recent(key, Some(1)));
|
||||
assert!(!done.is_recent(key, Some(2)), "another level");
|
||||
assert!(!done.is_recent((key.0, 1), Some(1)), "another page");
|
||||
let done = Done { bytes: Arc::downgrade(&bytes), key, level: Some(1), at: Instant::now(), epoch: 3 };
|
||||
assert!(done.is_recent(key, Some(1), 3));
|
||||
assert!(!done.is_recent(key, Some(2), 3), "another level");
|
||||
assert!(!done.is_recent((key.0, 1), Some(1), 3), "another page");
|
||||
assert!(!done.is_recent(key, Some(1), 4), "the cache was emptied since");
|
||||
drop(bytes);
|
||||
assert!(!done.is_recent(key, Some(1)), "the image is gone");
|
||||
assert!(!done.is_recent(key, Some(1), 3), "the image is gone");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,18 +771,6 @@ fn reserve(size: usize, id: usize, mine: Option<&AtomicUsize>) -> (Release<'_>,
|
||||
(Release(mine, size), now)
|
||||
}
|
||||
|
||||
/// Bytes a decode of `w` holds at its peak, with headroom for temporary buffers inside the
|
||||
/// format decoders: 0 when its level is halved from a cached one.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn decode_reservation(w: &Want) -> usize {
|
||||
let key = (Arc::as_ptr(&w.bytes) as usize, w.page);
|
||||
if dims_of(&w.bytes, w.page).is_some_and(|size| has_level(key, level_for(size, w.width_pt, w.on_screen))) {
|
||||
return 0;
|
||||
}
|
||||
let peak = reduced_estimates(std::slice::from_ref(w)).map_or_else(|| decode_sizes(&w.bytes, w.page).1, |(_, peak)| peak);
|
||||
peak.saturating_mul(3) / 2
|
||||
}
|
||||
|
||||
/// Decode the images in `wants` that are not cached yet on several threads at once, with the
|
||||
/// mip levels they are drawn at, so the render that draws them next finds them cached instead
|
||||
/// of decoding them one after another. Does nothing when fewer than two images need decoding.
|
||||
@@ -673,6 +783,18 @@ pub(crate) fn warm(wants: Vec<Want>) -> usize {
|
||||
if workers < 2 {
|
||||
return 0;
|
||||
}
|
||||
// Sizing reads image headers: a parser panic skips warming, and the render draws the images
|
||||
// as it would without it.
|
||||
let Ok(jobs) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plan(wants))) else { return 0 };
|
||||
if jobs.len() < 2 {
|
||||
return 0;
|
||||
}
|
||||
run_jobs(workers, jobs)
|
||||
}
|
||||
|
||||
/// [`warm`]'s decodes: the bytes to reserve for each and the drawings it serves. Empty when
|
||||
/// fewer than two images need decoding.
|
||||
fn plan(wants: Vec<Want>) -> Vec<(usize, Vec<Want>)> {
|
||||
// What is drawn of each asset and page, in drawing order.
|
||||
let mut assets: Vec<((usize, u32), Vec<Want>)> = Vec::new();
|
||||
let mut index: HashMap<(usize, u32), usize> = HashMap::new();
|
||||
@@ -720,7 +842,7 @@ pub(crate) fn warm(wants: Vec<Want>) -> usize {
|
||||
});
|
||||
// One image to decode gains nothing from threads (and is not worth sizing).
|
||||
if assets.iter().filter(|(key, _)| !held.contains_key(key)).count() < 2 {
|
||||
return 0;
|
||||
return Vec::new();
|
||||
}
|
||||
let mut pixels = held.values().fold(0usize, |n, b| n.saturating_add(*b));
|
||||
let mut jobs: Vec<(usize, Vec<Want>)> = Vec::new();
|
||||
@@ -728,6 +850,7 @@ pub(crate) fn warm(wants: Vec<Want>) -> usize {
|
||||
if held.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
fault(key.0);
|
||||
let (size, decoding) = reduced_estimates(&wanted).unwrap_or_else(|| {
|
||||
let full = wanted.first().map_or((0, 0), |w| decode_sizes(&w.bytes, w.page));
|
||||
// Only the levels drawn are kept.
|
||||
@@ -754,9 +877,11 @@ pub(crate) fn warm(wants: Vec<Want>) -> usize {
|
||||
// temporary buffers inside individual format decoders.
|
||||
jobs.push((decoding.saturating_mul(3) / 2, wanted));
|
||||
}
|
||||
if jobs.len() < 2 {
|
||||
return 0;
|
||||
}
|
||||
jobs
|
||||
}
|
||||
|
||||
/// Run [`warm`]'s `jobs` on up to `workers` threads; returns the most bytes reserved at once.
|
||||
fn run_jobs(workers: usize, jobs: Vec<(usize, Vec<Want>)>) -> usize {
|
||||
// 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).
|
||||
@@ -803,7 +928,7 @@ fn waited(id: usize) {
|
||||
/// Level `level` of a JPEG for [`mip`], decoded at 1/2, 1/4 or 1/8 of its size straight from
|
||||
/// its blocks and halved from there, so a screen-sized level needs no full-size decode. `None`
|
||||
/// when the full size is drawn, for other formats and for JPEGs only a full decode reads (CMYK).
|
||||
fn reduced(bytes: &Arc<Vec<u8>>, page: u32, level: u8) -> Option<Arc<Pixmap>> {
|
||||
fn reduced(bytes: &Arc<Vec<u8>>, page: u32, level: u8, gate: bool) -> Option<Arc<Pixmap>> {
|
||||
if level == 0 || page != 0 || !bytes.starts_with(&[0xFF, 0xD8]) {
|
||||
return None;
|
||||
}
|
||||
@@ -815,9 +940,16 @@ fn reduced(bytes: &Arc<Vec<u8>>, page: u32, level: u8) -> Option<Arc<Pixmap>> {
|
||||
// Cut to `halve`'s size (it rounds down, the decoder up), as the level a full
|
||||
// decode makes.
|
||||
let (sw, sh) = halved(crate::image_size(bytes)?, shrink);
|
||||
let p = decode_pixmap_reduced(bytes, shrink);
|
||||
// Gated, a JPEG without an estimate (one only a full decode reads) isn't tried here.
|
||||
let _reserved = if gate {
|
||||
let (_, _, peak) = designcraft_images::reduced_jpeg_estimate(bytes, shrink)?;
|
||||
Some(reserve(peak.saturating_mul(3) / 2, id, None))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let p = decode_pixmap_reduced(bytes, shrink)?;
|
||||
log_decode(id);
|
||||
let p = Arc::new(crop(p?, sw, sh)?);
|
||||
let p = Arc::new(crop(p, sw, sh)?);
|
||||
store(bytes, page, shrink, p.clone());
|
||||
p
|
||||
}
|
||||
@@ -855,7 +987,9 @@ pub fn clear() {
|
||||
c.map.clear();
|
||||
c.bad.clear();
|
||||
c.dims.clear();
|
||||
c.kept_full.clear();
|
||||
c.bytes = 0;
|
||||
c.epoch = c.epoch.wrapping_add(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1106,10 +1240,12 @@ mod tests {
|
||||
Arc::new(out.into_inner())
|
||||
}
|
||||
|
||||
/// Polls `mip_now` until it has the level asked for.
|
||||
/// Polls `mip_now` until it has the level asked for and the decode thread is done with it.
|
||||
fn ready(bytes: &Arc<Vec<u8>>, width_pt: f64, on_screen: f64) -> Option<Arc<Pixmap>> {
|
||||
for _ in 0..500 {
|
||||
if let Mip::Ready(pm) = mip_now(bytes, 0, width_pt, on_screen) {
|
||||
if let Mip::Ready(pm) = mip_now(bytes, 0, width_pt, on_screen)
|
||||
&& !background::is_busy(Arc::as_ptr(bytes) as usize)
|
||||
{
|
||||
return Some(pm);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
@@ -1132,8 +1268,12 @@ mod tests {
|
||||
assert_eq!((small.width(), small.height()), (128, 64));
|
||||
}
|
||||
|
||||
/// Taken by the tests that keep full decodes for zooming in: they evict each other's.
|
||||
static ZOOMING: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn zooming_in_step_by_step_decodes_an_image_at_most_twice() {
|
||||
let _zooming = ZOOMING.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let bytes = png_sized(1024, 1024);
|
||||
let since = mark();
|
||||
// 8 px per point, from 1/16 to 4 device pixels per point: level 4 (64 px) to level 0.
|
||||
@@ -1226,4 +1366,77 @@ mod tests {
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
assert!(decoders(&bytes, since).is_empty(), "not decoded again, in place or in the background");
|
||||
}
|
||||
|
||||
fn fault_on(bytes: &Arc<Vec<u8>>, on: bool) {
|
||||
let id = Arc::as_ptr(bytes) as usize;
|
||||
let mut f = FAULTS.lock().unwrap();
|
||||
f.retain(|i| *i != id);
|
||||
if on {
|
||||
f.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop every level of `bytes`, as an eviction does.
|
||||
fn evict(bytes: &Arc<Vec<u8>>) {
|
||||
let id = Arc::as_ptr(bytes) as usize;
|
||||
with(|c| {
|
||||
let gone: Vec<_> = c.map.keys().filter(|k| k.0 == id).copied().collect();
|
||||
for k in gone {
|
||||
if let Some(e) = c.map.remove(&k) {
|
||||
c.bytes -= size_of(&e.pm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_panic_in_a_background_decode_marks_the_image_bad_and_the_worker_goes_on() {
|
||||
let bytes = png_sized(64, 64);
|
||||
let id = Arc::as_ptr(&bytes) as usize;
|
||||
fault_on(&bytes, true);
|
||||
let mut failed = false;
|
||||
for _ in 0..500 {
|
||||
if matches!(mip_now(&bytes, 0, 64.0, 1.0), Mip::Failed) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
fault_on(&bytes, false);
|
||||
assert!(failed, "marked bad");
|
||||
// Forgotten as bad, it decodes again: the job left the busy list and a thread runs it.
|
||||
with(|c| c.bad.remove(&(id, 0)));
|
||||
assert!(ready(&bytes, 64.0, 1.0).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_panic_while_warm_sizes_its_decodes_skips_warming() {
|
||||
let (a, b) = (png(8, 51), png(8, 53));
|
||||
fault_on(&a, true);
|
||||
let peak = warm(vec![want(&a), want(&b)]);
|
||||
fault_on(&a, false);
|
||||
assert_eq!(peak, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_turned_away_as_recent_is_drawn_when_its_window_passes() {
|
||||
let bytes = png_sized(96, 96);
|
||||
assert!(ready(&bytes, 96.0, 1.0).is_some());
|
||||
evict(&bytes);
|
||||
assert!(matches!(mip_now(&bytes, 0, 96.0, 1.0), Mip::Pending(None)));
|
||||
assert!(background::wake_scheduled(), "the generation advances once the window has passed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_most_two_full_decodes_are_kept_for_zooming_in() {
|
||||
let _zooming = ZOOMING.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let images: Vec<_> = (0..3).map(|_| png_sized(512, 512)).collect();
|
||||
for img in &images {
|
||||
// Level 2, then zoomed in to level 1: the full decode is kept.
|
||||
assert_eq!(mip(img, 0, 128.0, 0.5).map(|pm| pm.width()), Some(128));
|
||||
assert_eq!(mip(img, 0, 128.0, 1.0).map(|pm| pm.width()), Some(256));
|
||||
}
|
||||
let full: Vec<bool> = images.iter().map(|img| is_cached((Arc::as_ptr(img) as usize, 0, 0))).collect();
|
||||
assert_eq!(full, [false, true, true], "the oldest is let go");
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,8 @@ pub struct Renderer {
|
||||
cmyk_view: Option<blendspace::CachedView>,
|
||||
/// Whether the last render ran the CMYK ink passes (only then is its view worth keeping).
|
||||
cmyk_ran: bool,
|
||||
/// Whether the last render drew an image still decoding in the background (a stand-in).
|
||||
images_pending: bool,
|
||||
/// Drop shadow and outer glow images kept between renders.
|
||||
kept: fx::KeptEffects,
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user