From ba98dc66c924de8e60068243f4033e960a981501 Mon Sep 17 00:00:00 2001 From: tecari0 <121329615+tecari0@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:06:07 -0700 Subject: [PATCH] Detail: snap the 100% view to whole screen pixels (#1683) At 100% the photo's corner could sit half a physical pixel off, for example in a 1279.5 pt canvas on a 2x display. The GPU then resampled the photo with its bilinear filter and blurred it. On a 5152x7728 JPEG the 100% view kept 68% of the fine detail. fit_rect now rounds the corner to a whole physical pixel when the photo is 1:1 with the screen. Other zooms resample anyway, and a pinch must not jump, so they stay as they were. With the fix the same view matches the original (71.4 dB PSNR, 100% of the detail). Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/panels/detail.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 151d4a57a..ecbd7f1ea 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -122,7 +122,16 @@ pub(crate) fn fit_rect(area: Rect, aspect: f32, zoom: Zoom, img_px: [usize; 2], }; let pan = bounded_pan(area, vec2(w, h), pan); let c = pos2(area.center().x - (pan.0 - 0.5) * w, area.center().y - (pan.1 - 0.5) * h); - Rect::from_center_size(c, vec2(w, h)) + let rect = Rect::from_center_size(c, vec2(w, h)); + // At 100 % the texture maps texel for texel to screen pixels, so its corner must sit on a whole + // physical pixel: half a pixel off and the GPU's bilinear filter blurs the view (an odd-width + // canvas on a 2x display does this). Other zooms resample anyway, and a pinch must not jump. + let one_to_one = ppp.is_finite() && ppp > 0.0 && (w * ppp - img_px[0] as f32).abs() < 0.5; + if matches!(zoom, Zoom::Percent(_)) && one_to_one { + let snap = |v: f32| (v * ppp).round() / ppp; + return Rect::from_min_size(pos2(snap(rect.min.x), snap(rect.min.y)), rect.size()); + } + rect } /// Fit a decoded image inside the frame calculated from catalog metadata. Unsupported RAWs can @@ -2406,6 +2415,22 @@ mod preview_geometry_tests { assert!(frame.contains_rect(image)); } + /// A 100 % view must put the photo's corner on a whole physical pixel, or the GPU's bilinear + /// filter softens it (a 1279.5 pt canvas on a 2x display centred the photo half a pixel off). + #[test] + fn percent_zoom_lands_on_whole_physical_pixels() { + let area = Rect::from_min_size(pos2(0.0, 42.0), vec2(1279.5, 726.0)); + for ppp in [1.0, 1.25, 2.0, 3.0] { + for pan in [(0.5, 0.5), (0.3, 0.7), (0.4999, 0.5001)] { + let image = fit_rect(area, 5152.0 / 7728.0, Zoom::Percent(100.0), [5152, 7728], ppp, pan); + for v in [image.min.x, image.min.y] { + assert!((v * ppp - (v * ppp).round()).abs() < 1e-3, "{v} pt is not a whole pixel at {ppp}x, pan {pan:?}"); + } + assert!((image.width() * ppp - 5152.0).abs() < 1e-3, "size stays the photo's own pixels"); + } + } + } + #[test] fn portrait_preview_aspect_fits_detail_area_without_stretching() { let area = Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 200.0));