Never crash: Read the edge pixel for bilinear samples outside the picture

Infinite coordinates from a zero-size crop overflowed the sampler.
This commit is contained in:
Michael Anderson
2026-10-13 10:46:42 -05:00
parent f753778f45
commit 4a07254e06
4 changed files with 44 additions and 3 deletions
+22
View File
@@ -402,6 +402,28 @@ fn paste_selected_settings_pastes_only_chosen_copied_groups() {
assert_eq!(s.develop_of(ids[1]).unwrap().color.vibrance, 30.0);
}
/// A crop of no size (or a hair wide), as `crop.set` or a sidecar can set it, made the render's
/// samplers read at infinite coordinates and its blurs ask for sigmas of millions of pixels: with a
/// healing spot and masks on, render workers panicked (an i32 square in the blur, a bilinear
/// neighbour past isize::MAX). Such a photo renders, quickly.
#[test]
fn degenerate_crops_render_with_spots_and_masks() {
for rect in [[0.5, 0.5, 0.5, 0.5], [0.0, 0.0, 1e-9, 1.0]] {
let mut s = demo();
let id = s.active().unwrap();
s.execute("spot.add", &json!({"points": [[0.5, 0.5]], "size": 0.2, "mode": "heal"})).unwrap();
s.execute("mask.add", &json!({"kind": "radial", "rx": 0.3, "ry": 0.3})).unwrap();
s.execute("mask.add", &json!({"kind": "linear"})).unwrap();
s.execute("develop.set", &json!({"control": "effects.clarity", "value": 80})).unwrap();
s.execute("develop.set", &json!({"control": "effects.dehaze", "value": 50})).unwrap();
s.execute("crop.set", &json!({"rect": rect, "angle": 30})).unwrap();
let t = std::time::Instant::now();
let r = s.render_now(id, 700, 450).unwrap();
assert!(r.image.width >= 1 && r.image.height >= 1, "{rect:?}");
assert!(t.elapsed().as_secs_f64() < 60.0, "{rect:?}: {:?}", t.elapsed());
}
}
#[test]
fn masks_reorder_and_duplicate_and_invert() {
let mut s = demo();
+3 -1
View File
@@ -398,7 +398,9 @@ fn box_down(m: &Image<u8>, f: usize) -> Plane {
/// Bilinear sample at continuous pixel coordinates (pixel centres at +0.5), edges extended.
fn bilinear(p: &Plane, x: f32, y: f32) -> f32 {
let (fx, fy) = (x - 0.5, y - 0.5);
// (held a pixel outside, which reads the same edge: an infinite coordinate reads the edge
// instead of a NaN weight and a neighbour index past isize::MAX)
let (fx, fy) = (x.clamp(-1.0, p.width as f32 + 1.0) - 0.5, y.clamp(-1.0, p.height as f32 + 1.0) - 0.5);
let (x0, y0) = (fx.floor(), fy.floor());
let (tx, ty) = (fx - x0, fy - y0);
let (x0, y0) = (x0 as isize, y0 as isize);
+17 -2
View File
@@ -206,8 +206,11 @@ impl Rgb32f {
/// Bilinear sample at continuous pixel coordinates (pixel centres at +0.5).
#[inline]
pub fn sample_bilinear(&self, x: f32, y: f32) -> [f32; 3] {
let fx = x - 0.5;
let fy = y - 0.5;
// a pixel past the edge reads the edge pixel, so a coordinate held a pixel outside gives
// the same sample; an infinite one (a crop of no size's inverse mapping) then reads the
// edge too, instead of a NaN weight and a neighbour index past isize::MAX
let fx = x.clamp(-1.0, self.width as f32 + 1.0) - 0.5;
let fy = y.clamp(-1.0, self.height as f32 + 1.0) - 0.5;
let x0 = fx.floor();
let y0 = fy.floor();
let tx = fx - x0;
@@ -258,6 +261,18 @@ mod tests {
use super::*;
use lightcraft_geom::Orientation;
/// A sample at an infinite coordinate (the inverse mapping of a crop of no size) cast to
/// isize::MAX, and its right / lower neighbour overflowed (a panic where overflow is checked):
/// the neighbours saturate and the edge pixel comes back.
#[test]
fn bilinear_samples_at_infinite_coordinates_read_the_edge() {
let img = Rgb32f::from_fn(3, 2, |x, y| [x as f32, y as f32, 1.0]);
assert_eq!(img.sample_bilinear(f32::INFINITY, f32::INFINITY), [2.0, 1.0, 1.0]);
assert_eq!(img.sample_bilinear(f32::MAX, 0.0), [2.0, 0.0, 1.0]);
assert_eq!(img.sample_bilinear(f32::NEG_INFINITY, f32::NEG_INFINITY), [0.0, 0.0, 1.0]);
assert_eq!(img.sample_bilinear(1.5, 0.5), [1.0, 0.0, 1.0], "an ordinary sample is as before");
}
#[test]
fn crop_and_rotate() {
let img = Image::<u32>::from_fn(4, 3, |x, y| (y * 10 + x) as u32);
+2
View File
@@ -5,6 +5,8 @@
### Reliability
- A crop a hair wide (or of no size), set by an agent or read from a sidecar, no longer crashes or hangs the
render with Clarity, Dehaze, Texture or Highlights / Shadows on: their blurs are held to the picture's size.
- A crop of no size with healing spots or masks no longer crashes the render: samples outside the picture read its
edge.
### Slider drags on large raws
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you