perf(thumbnails): SIMD resize via fast_image_resize (PNG 2.6x, RAM 2.5x lower)

Replace the image crate's scalar resampler with fast_image_resize
(AVX2/SSE4.1/NEON) in a shared encode_thumbnail() helper. render_all now
converts to RGB8 once and SIMD-resizes the shared buffer per size. Lanczos3 for
downscaling, CatmullRom when upscaling (Lanczos rings on enlargement).

Also folds the duplicated path-variant generate_all_sizes_background into the
shared render path -- it had missed BOTH shrink-on-load and SIMD resizing -- so
every thumbnail path now goes through one optimised routine (no duplication).

Measured on 14 cores vs the post-1.5 state (benches/BASELINE.md):
- PNG 2.60x faster (33.6->12.9ms), GIF/WebP 1.25-1.6x: full-resolution decode
  paths where the resize dominates, so SIMD helps most
- JPEG only ~7% (shrink-on-load already shrank the bitmap) but peak heap fell
  another ~2.5x (17.6->7.1MB): tight RGB buffers, RGB conversion once
- quality SSIM 0.986-0.994 at identical dims (>=0.98 gate)

Thumbnails are now exactly max_dim on the long side (e.g. 400x266) vs the old
fit-within 399x266 -- a <=1px change, invisible under object-fit: cover.

Bench example gains an exact-dims quality reference + semaphore throughput table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-21 15:53:28 +02:00
parent 51713b218d
commit b7e092ad05
5 changed files with 177 additions and 125 deletions
+16 -7
View File
@@ -453,20 +453,29 @@ fn measure_semaphore_throughput(case: &CorpusCase, permits: usize, window: Durat
// Quality verification helpers (Table C)
// ---------------------------------------------------------------------------
/// Reference thumbnail: identical resample + q80 JPEG encode as production, but
/// forced through a **full decode** (no shrink-on-load). Comparing against this
/// isolates exactly the quality impact of DCT scale-on-decode. EXIF orientation
/// is not applied here, so only run it on orientation=1 corpus cases.
/// Reference thumbnail: a **full-resolution decode** + high-quality CatmullRom
/// resample + q80 JPEG encode — the original (pre-optimisation) quality target.
/// Comparing the optimised output against this gauges whether shrink-on-load +
/// SIMD resizing degrades quality. Uses the same exact target dims as production
/// (`resize_exact`) so the comparison is apples-to-apples, never a dim mismatch.
/// EXIF orientation is not applied, so only run it on orientation=1 cases.
fn reference_render_full_decode(bytes: &[u8], max_dim: u32) -> Vec<u8> {
let img = image::load_from_memory(bytes).expect("ref full decode");
let (ow, oh) = (img.width(), img.height());
// Same fit-to-longest-side dims production computes (see fit_dims()).
let (nw, nh) = if ow > oh {
(max_dim, (oh as f32 * (max_dim as f32 / ow as f32)) as u32)
(
max_dim,
((oh as f32 * (max_dim as f32 / ow as f32)) as u32).max(1),
)
} else {
((ow as f32 * (max_dim as f32 / oh as f32)) as u32, max_dim)
(
((ow as f32 * (max_dim as f32 / oh as f32)) as u32).max(1),
max_dim,
)
};
let rgb = img
.resize(nw, nh, image::imageops::FilterType::CatmullRom)
.resize_exact(nw, nh, image::imageops::FilterType::CatmullRom)
.to_rgb8();
let mut buf = Vec::new();
let enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 80);