diff --git a/crates/algo/src/oil.rs b/crates/algo/src/oil.rs index 73645619ec..a894efc2ca 100644 --- a/crates/algo/src/oil.rs +++ b/crates/algo/src/oil.rs @@ -31,11 +31,14 @@ fn stroke_len(stylization: f32) -> f32 { } /// `f32::round` (half away from zero) as an integer, without the `roundf` call `f32::round` -/// makes on the baseline x86-64 target. Exact for `|v| < 2^22`, which covers every window -/// coordinate; larger magnitudes saturate like the cast the caller used to perform. +/// makes on the baseline x86-64 target. The sum is taken in `f64`, where `v ± 0.5` is exact for +/// every `f32` the cast does not saturate (an `f32` sum is not: `0.49999997 + 0.5` rounds up to +/// `1.0`), so the result equals `v.round() as i32` for every `f32`, NaN (0) and the saturating +/// ends included. #[inline] fn round_i32(v: f32) -> i32 { - (v + 0.5f32.copysign(v)) as i32 + let v = f64::from(v); + (v + 0.5f64.copysign(v)) as i32 } /// [`round_i32`] clamped into `0..n`. @@ -231,10 +234,40 @@ mod tests { } v += 0.25; } + // The binade crossings where an `f32` sum `v + 0.5` rounds up (0.5 - 2^-25), the ties, and + // the largest non-integer `f32`. + let below_half = f32::from_bits(0x3eff_ffff); + for v in [below_half, -below_half, 0.5, -0.5, 1.5, -1.5, 2.5, -2.5, 8_388_607.5, -8_388_607.5, 8_388_608.0, 3.0e9, -3.0e9] { + assert_eq!(round_i32(v), v.round() as i32, "round_i32({v:e})"); + for n in [1usize, 2, 33] { + let want = (v.round().clamp(0.0, (n - 1) as f32)) as usize; + assert_eq!(round_index(v, n), want, "round_index({v:e}, {n})"); + } + } assert_eq!(round_i32(f32::NAN), f32::NAN.round() as i32); for n in [1usize, 8] { assert_eq!(round_index(f32::NEG_INFINITY, n), (f32::NEG_INFINITY.clamp(0.0, (n - 1) as f32)) as usize); assert_eq!(round_index(f32::INFINITY, n), ((n - 1) as f32) as usize); } } + + /// Every `f32` bit pattern in [-4096, 4096] (both signs, about 2.3e9 values, split across + /// threads) rounds exactly like `f32::round`. + #[test] + fn rounding_matches_the_standard_library_for_every_f32_up_to_4096() { + use rayon::prelude::*; + let last = 4096.0f32.to_bits(); + let bad = (0..=last >> 20) + .into_par_iter() + .filter_map(|hi| { + let lo = hi << 20; + let hi_end = (lo | 0xf_ffff).min(last); + (lo..=hi_end).find_map(|bits| { + let v = f32::from_bits(bits); + [v, -v].into_iter().find(|v| round_i32(*v) != v.round() as i32) + }) + }) + .min_by(|a, b| a.abs().total_cmp(&b.abs())); + assert_eq!(bad, None, "round_i32 differs from f32::round"); + } } diff --git a/docs/oil-paint-performance.md b/docs/oil-paint-performance.md index 2349125258..531b47ecad 100644 --- a/docs/oil-paint-performance.md +++ b/docs/oil-paint-performance.md @@ -12,10 +12,12 @@ filter's time went. - **`sample()`:** `x.floor() as usize` became `x as usize`. The two arguments are clamped to `0 ..= w-1` on the lines above, so truncation is floor for every input the function can see (NaN truncates to 0 either way, which is the same sample the old code read). -- **Tangent and bristle lookups:** `round` became `round_i32(v) = (v + 0.5 * sign(v)) as i32`. - `f32::round` is round-half-away-from-zero, which is truncation of `v ± 0.5`; that sum is exact - in `f32` for `|v| < 2^22` (the spacing there is at most 0.5), and every coordinate is inside a - tile window, so the integer is identical. `round_index` adds the `clamp(0, n-1)` the tangent +- **Tangent and bristle lookups:** `round` became `round_i32(v) = (v as f64 + 0.5 * sign(v)) as i32`. + `f32::round` is round-half-away-from-zero, which is truncation of `v ± 0.5` when that sum is + exact. In `f32` it is not always: `0.49999997 + 0.5` (`0.5 - 2^-25`) rounds up to `1.0`, so an + `f32` sum gives 1 where `f32::round` gives 0. In `f64` the sum is exact for every `f32` the + `i32` cast does not saturate, so the integer is identical for every input; a unit test checks + every `f32` bit pattern in [-4096, 4096] against `f32::round`. `round_index` adds the `clamp(0, n-1)` the tangent lookup already applied; NaN casts to 0 and ±∞ casts to an end of the clamp range in both versions, so the clamped results match for every input, including non-finite ones.