This commit is contained in:
andrews05 2026-05-31 00:48:46 +08:00 committed by GitHub
commit a8bcb49104
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 175 additions and 84 deletions

View file

@ -35,7 +35,7 @@ pub(crate) struct Candidate {
} }
impl Candidate { impl Candidate {
fn cmp_key(&self) -> impl Ord + use<> { pub fn cmp_key(&self) -> impl Ord + use<> {
( (
self.estimated_output_size, self.estimated_output_size,
self.image.data.len(), self.image.data.len(),
@ -56,6 +56,7 @@ pub(crate) struct Evaluator {
nth: AtomicUsize, nth: AtomicUsize,
executed: Arc<AtomicUsize>, executed: Arc<AtomicUsize>,
best_candidate_size: Arc<AtomicMin>, best_candidate_size: Arc<AtomicMin>,
threshold: f64,
/// images are sent to the caller thread for evaluation /// images are sent to the caller thread for evaluation
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
eval_channel: (Sender<Candidate>, Receiver<Candidate>), eval_channel: (Sender<Candidate>, Receiver<Candidate>),
@ -70,6 +71,7 @@ impl Evaluator {
filters: IndexSet<FilterStrategy>, filters: IndexSet<FilterStrategy>,
deflater: Deflater, deflater: Deflater,
optimize_alpha: bool, optimize_alpha: bool,
threshold: f64,
final_round: bool, final_round: bool,
) -> Self { ) -> Self {
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
@ -83,6 +85,7 @@ impl Evaluator {
nth: AtomicUsize::new(0), nth: AtomicUsize::new(0),
executed: Arc::new(AtomicUsize::new(0)), executed: Arc::new(AtomicUsize::new(0)),
best_candidate_size: Arc::new(AtomicMin::new(None)), best_candidate_size: Arc::new(AtomicMin::new(None)),
threshold,
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
eval_channel, eval_channel,
#[cfg(not(feature = "parallel"))] #[cfg(not(feature = "parallel"))]
@ -93,7 +96,7 @@ impl Evaluator {
/// Wait for all evaluations to finish and return smallest reduction /// Wait for all evaluations to finish and return smallest reduction
/// Or `None` if the queue is empty. /// Or `None` if the queue is empty.
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
pub fn get_best_candidate(self) -> Option<Candidate> { pub fn get_best_candidates(self, limit: usize) -> Vec<Candidate> {
let (eval_send, eval_recv) = self.eval_channel; let (eval_send, eval_recv) = self.eval_channel;
// Disconnect the sender, breaking the loop in the thread // Disconnect the sender, breaking the loop in the thread
drop(eval_send); drop(eval_send);
@ -103,17 +106,28 @@ impl Evaluator {
while self.executed.load(Relaxed) < nth { while self.executed.load(Relaxed) < nth {
rayon::yield_local(); rayon::yield_local();
} }
eval_recv.into_iter().min_by_key(Candidate::cmp_key) let mut candidates: Vec<_> = eval_recv.iter().collect();
if candidates.is_empty() {
return candidates;
}
candidates.sort_by_key(Candidate::cmp_key);
let best_size = self.best_candidate_size.get().unwrap();
candidates
.into_iter()
.take(limit)
.filter(|c| c.estimated_output_size <= best_size)
.collect()
} }
#[cfg(not(feature = "parallel"))] #[cfg(not(feature = "parallel"))]
pub fn get_best_candidate(self) -> Option<Candidate> { pub fn get_best_candidates(self, _limit: usize) -> Vec<Candidate> {
self.eval_best_candidate.into_inner() vec![]
} }
/// Set best size, if known in advance /// Set best size, if known in advance
pub fn set_best_size(&self, size: usize) { pub fn set_best_size(&self, size: usize) {
self.best_candidate_size.set_min(size); self.best_candidate_size
.set_min(size + (self.threshold * size as f64) as usize);
} }
/// Check if the image is smaller than others /// Check if the image is smaller than others
@ -133,6 +147,7 @@ impl Evaluator {
let final_round = self.final_round; let final_round = self.final_round;
let executed = self.executed.clone(); let executed = self.executed.clone();
let best_candidate_size = self.best_candidate_size.clone(); let best_candidate_size = self.best_candidate_size.clone();
let threshold = self.threshold;
let description = description.to_string(); let description = description.to_string();
// sends it off asynchronously for compression, // sends it off asynchronously for compression,
// but results will be collected via the message queue // but results will be collected via the message queue
@ -153,30 +168,22 @@ impl Evaluator {
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha); let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get()); let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data { if let Ok(idat_data) = idat_data {
let estimated_output_size = image.estimated_output_size(&idat_data); let size = image.estimated_output_size(&idat_data);
trace!( trace!(
"Eval: {}-bit {:23} {:8} {} bytes", "Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth, description, filter, estimated_output_size image.ihdr.bit_depth, description, filter, size
); );
// Skip if it exceeds best known size. (This is important to ensure
// the evaluator returns no result when all candidates are too large.)
if let Some(max) = best_candidate_size.get() {
if estimated_output_size > max {
return;
}
}
// We only need to retain the IDAT data in the final round // We only need to retain the IDAT data in the final round
let new = Candidate { let new = Candidate {
image: image.clone(), image: image.clone(),
idat_data: if final_round { Some(idat_data) } else { None }, idat_data: if final_round { Some(idat_data) } else { None },
estimated_output_size, estimated_output_size: size,
filter: filter.clone(), filter: filter.clone(),
filter_used, filter_used,
nth, nth,
}; };
best_candidate_size.set_min(estimated_output_size); best_candidate_size.set_min(size + (threshold * size as f64) as usize);
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
{ {
@ -199,4 +206,73 @@ impl Evaluator {
}); });
}); });
} }
pub fn try_candidates(&self, candidates: Vec<Candidate>) {
let nth = self.nth.fetch_add(1, SeqCst);
// These clones are only cheap refcounts
let deadline = self.deadline.clone();
let deflater = self.deflater;
let optimize_alpha = self.optimize_alpha;
let final_round = self.final_round;
let executed = self.executed.clone();
let best_candidate_size = self.best_candidate_size.clone();
let threshold = self.threshold;
// sends it off asynchronously for compression,
// but results will be collected via the message queue
#[cfg(feature = "parallel")]
let eval_send = self.eval_channel.0.clone();
rayon::spawn(move || {
executed.fetch_add(1, Relaxed);
// Updating of best result inside the parallel loop would require locks,
// which are dangerous to do in side Rayon's loop.
// Instead, only update (atomic) best size in real time,
// and the best result later without need for locks.
candidates.par_iter().with_max_len(1).for_each(|candidate| {
if deadline.passed() {
return;
}
let description = format!("{}", candidate.image.ihdr.color_type);
let (filtered, filter_used) = candidate
.image
.filter_image(candidate.filter_used.clone(), optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data {
let size = candidate.image.estimated_output_size(&idat_data);
// For the final round we need the IDAT data, otherwise the filtered data
let new = Candidate {
image: candidate.image.clone(),
idat_data: if final_round { Some(idat_data) } else { None },
estimated_output_size: size,
filter: candidate.filter.clone(),
filter_used,
nth,
};
best_candidate_size.set_min(size + (threshold * size as f64) as usize);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
new.image.ihdr.bit_depth, description, new.filter, size
);
#[cfg(feature = "parallel")]
{
eval_send.send(new).expect("send");
}
#[cfg(not(feature = "parallel"))]
{
match &mut *self.eval_best_candidate.borrow_mut() {
Some(prev) if prev.cmp_key() < new.cmp_key() => {}
best => *best = Some(new),
}
}
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
trace!(
"Eval: {}-bit {:23} {:8} >{} bytes",
candidate.image.ihdr.bit_depth, description, candidate.filter, size
);
}
});
});
}
} }

View file

@ -395,10 +395,8 @@ fn optimize_raw(
// 9 is not appreciably better than 8 // 9 is not appreciably better than 8
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level // 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
let compression = match opts.deflater { let compression = match opts.deflater {
Deflater::Libdeflater { compression } => { Deflater::Libdeflater { compression } => 7.min(compression),
if opts.fast_evaluation { 7 } else { 8 }.min(compression) _ => 7,
}
_ => 8,
}; };
let eval_deflater = Deflater::Libdeflater { compression }; let eval_deflater = Deflater::Libdeflater { compression };
// If only one filter is selected, use this for evaluations // If only one filter is selected, use this for evaluations
@ -414,16 +412,15 @@ fn optimize_raw(
eval_filters.clone(), eval_filters.clone(),
eval_deflater, eval_deflater,
false, false,
opts.candidate_threshold,
opts.deflater == eval_deflater, opts.deflater == eval_deflater,
); );
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval); let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
let eval_result = eval.get_best_candidate(); let eval_results = eval.get_best_candidates(opts.max_candidates);
if let Some(ref result) = eval_result { if let Some(result) = eval_results.first() {
new_image = result.image.clone(); new_image = result.image.clone();
} }
let reduction_occurred = new_image.ihdr.color_type != image.ihdr.color_type let reduction_occurred = !new_image.is_same_format(&image);
|| new_image.ihdr.bit_depth != image.ihdr.bit_depth
|| new_image.ihdr.interlaced != image.ihdr.interlaced;
if reduction_occurred { if reduction_occurred {
report_format("Transformed image to ", &new_image); report_format("Transformed image to ", &new_image);
@ -435,7 +432,7 @@ fn optimize_raw(
opts, opts,
deadline, deadline,
max_size, max_size,
eval_result, eval_results,
eval_filters, eval_filters,
eval_deflater, eval_deflater,
); );
@ -443,7 +440,8 @@ fn optimize_raw(
} else { } else {
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline, // If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
// we should still check if the evaluator compressed the baseline smaller than the original. // we should still check if the evaluator compressed the baseline smaller than the original.
(eval_result?, eval_deflater) let result = eval_results.into_iter().next();
(result?, eval_deflater)
}; };
if result.idat_data.is_some() if result.idat_data.is_some()
@ -462,58 +460,52 @@ fn perform_trials(
opts: &Options, opts: &Options,
deadline: Arc<Deadline>, deadline: Arc<Deadline>,
max_size: Option<usize>, max_size: Option<usize>,
mut eval_result: Option<Candidate>, mut eval_results: Vec<Candidate>,
eval_filters: IndexSet<FilterStrategy>, eval_filters: IndexSet<FilterStrategy>,
eval_deflater: Deflater, eval_deflater: Deflater,
) -> Option<Candidate> { ) -> Option<Candidate> {
// First perform a fast evaluation of selected filters
let mut filters = opts.filters.clone(); let mut filters = opts.filters.clone();
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some()); if !eval_results.is_empty() {
if fast_eval { // Some filters have already been evaluated, we don't need to try them again
// Perform a fast evaluation of selected filters followed by a single main compression trial filters = filters.difference(&eval_filters).cloned().collect();
}
if eval_result.is_some() { if !filters.is_empty() {
// Some filters have already been evaluated, we don't need to try them again trace!("Evaluating {} filters", filters.len());
filters = filters.difference(&eval_filters).cloned().collect(); let eval = Evaluator::new(
} deadline.clone(),
filters.clone(),
if !filters.is_empty() { eval_deflater,
trace!("Evaluating {} filters", filters.len()); opts.optimize_alpha,
let eval = Evaluator::new( opts.candidate_threshold,
deadline, opts.deflater == eval_deflater,
filters, );
eval_deflater, if let Some(best) = eval_results.first() {
opts.optimize_alpha, eval.set_best_size(best.estimated_output_size);
opts.deflater == eval_deflater, for result in &eval_results {
); eval.try_image(result.image.clone());
if let Some(result) = &eval_result {
eval.set_best_size(result.estimated_output_size);
} }
} else {
eval.try_image(image.clone()); eval.try_image(image.clone());
if let Some(result) = eval.get_best_candidate() {
eval_result = Some(result);
}
} }
let mut results = eval.get_best_candidates(opts.max_candidates);
// We should have a result here - fail if not (e.g. deadline passed) if !results.is_empty() {
let mut result = eval_result?; eval_results.append(&mut results);
eval_results.sort_by_key(Candidate::cmp_key);
if result.idat_data.is_none() { let mut best_size = eval_results.first().unwrap().estimated_output_size;
// Compress with the main deflater best_size += (opts.candidate_threshold * best_size as f64) as usize;
debug!("Trying filter {} with {}", result.filter, opts.deflater); eval_results = eval_results
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha); .into_iter()
match opts.deflater.deflate(&data, max_size) { .take(opts.max_candidates)
Ok(idat_data) => { .filter(|c| c.estimated_output_size <= best_size)
result.estimated_output_size = result.image.estimated_output_size(&idat_data); .collect();
result.idat_data = Some(idat_data);
trace!("{} bytes", result.estimated_output_size);
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(">{bytes} bytes");
}
Err(_) => (),
}
} }
return Some(result); }
// Return best result if this is the final round
if opts.deflater == eval_deflater {
return eval_results.into_iter().next();
} }
// Perform full compression trials of selected filters and determine the best // Perform full compression trials of selected filters and determine the best
@ -530,12 +522,23 @@ fn perform_trials(
} }
debug!("Trying {} filters with {}", filters.len(), opts.deflater); debug!("Trying {} filters with {}", filters.len(), opts.deflater);
let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true); let eval = Evaluator::new(
deadline,
filters,
opts.deflater,
opts.optimize_alpha,
opts.candidate_threshold,
true,
);
if let Some(max_size) = max_size { if let Some(max_size) = max_size {
eval.set_best_size(max_size); eval.set_best_size(max_size);
} }
eval.try_image(image); if eval_results.is_empty() {
eval.get_best_candidate() eval.try_image(image);
} else {
eval.try_candidates(eval_results);
}
eval.get_best_candidates(1).into_iter().next()
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -149,6 +149,8 @@ pub struct Options {
/// ///
/// Default: `true` /// Default: `true`
pub fast_evaluation: bool, pub fast_evaluation: bool,
pub max_candidates: usize,
pub candidate_threshold: f64,
/// Maximum amount of time to spend on optimizations. /// Maximum amount of time to spend on optimizations.
/// Further potential optimizations are skipped if the timeout is exceeded. /// Further potential optimizations are skipped if the timeout is exceeded.
/// ///
@ -204,7 +206,6 @@ impl Options {
} }
fn apply_preset_3(mut self) -> Self { fn apply_preset_3(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! { self.filters = indexset! {
FilterStrategy::NONE, FilterStrategy::NONE,
FilterStrategy::Bigrams, FilterStrategy::Bigrams,
@ -214,11 +215,12 @@ impl Options {
level: 1, level: 1,
}, },
}; };
self.max_candidates = 2;
self.candidate_threshold = 0.05;
self self
} }
fn apply_preset_4(mut self) -> Self { fn apply_preset_4(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! { self.filters = indexset! {
FilterStrategy::NONE, FilterStrategy::NONE,
FilterStrategy::Bigrams, FilterStrategy::Bigrams,
@ -229,11 +231,12 @@ impl Options {
}, },
}; };
self.deflater = Deflater::Libdeflater { compression: 12 }; self.deflater = Deflater::Libdeflater { compression: 12 };
self.max_candidates = 4;
self.candidate_threshold = 0.05;
self self
} }
fn apply_preset_5(mut self) -> Self { fn apply_preset_5(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! { self.filters = indexset! {
FilterStrategy::NONE, FilterStrategy::NONE,
FilterStrategy::SUB, FilterStrategy::SUB,
@ -248,11 +251,12 @@ impl Options {
}, },
}; };
self.deflater = Deflater::Libdeflater { compression: 12 }; self.deflater = Deflater::Libdeflater { compression: 12 };
self.max_candidates = 8;
self.candidate_threshold = 0.1;
self self
} }
fn apply_preset_6(mut self) -> Self { fn apply_preset_6(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! { self.filters = indexset! {
FilterStrategy::NONE, FilterStrategy::NONE,
FilterStrategy::SUB, FilterStrategy::SUB,
@ -269,6 +273,8 @@ impl Options {
}, },
}; };
self.deflater = Deflater::Libdeflater { compression: 12 }; self.deflater = Deflater::Libdeflater { compression: 12 };
self.max_candidates = 16;
self.candidate_threshold = 0.2;
self self
} }
} }
@ -296,6 +302,8 @@ impl Default for Options {
strip: StripChunks::None, strip: StripChunks::None,
deflater: Deflater::Libdeflater { compression: 11 }, deflater: Deflater::Libdeflater { compression: 11 },
fast_evaluation: true, fast_evaluation: true,
max_candidates: 1,
candidate_threshold: 0.0,
timeout: None, timeout: None,
max_decompressed_size: None, max_decompressed_size: None,
} }

View file

@ -262,6 +262,13 @@ impl PngImage {
Ok(image) Ok(image)
} }
#[must_use]
pub fn is_same_format(&self, other: &Self) -> bool {
self.ihdr.color_type != other.ihdr.color_type
|| self.ihdr.bit_depth != other.ihdr.bit_depth
|| self.ihdr.interlaced != other.ihdr.interlaced
}
/// Enable or disable interlacing /// Enable or disable interlacing
/// Returns the new image if the interlacing was changed, None otherwise /// Returns the new image if the interlacing was changed, None otherwise
/// Assumes that the data has already been de-filtered /// Assumes that the data has already been de-filtered

View file

@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage}; use crate::{ColorType, Deadline, Options, evaluate::Evaluator, png::PngImage};
pub mod alpha; pub mod alpha;
use crate::alpha::*; use crate::alpha::*;
@ -21,10 +21,7 @@ pub(crate) fn perform_reductions(
// At low compression levels, skip some transformations which are less likely to be effective // At low compression levels, skip some transformations which are less likely to be effective
// This currently affects optimization presets 0-2 // This currently affects optimization presets 0-2
let cheap = match opts.deflater { let cheap = opts.max_candidates == 1;
Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
_ => false,
};
// Interlacing must be processed first in order to evaluate the rest correctly // Interlacing must be processed first in order to evaluate the rest correctly
if let Some(interlacing) = opts.interlace { if let Some(interlacing) = opts.interlace {