fix(algo): keep the oil-paint rounding exact just below a half

`round_i32` added the half in `f32`, where a value within one ulp of a
half-integer can round across it: `f32::from_bits(0x3eff_ffff)`, the largest
`f32` below `0.5`, plus `0.5` rounds up to `1.0`, so the helper returned `1`
where `f32::round` returns `0` and the LIC could select a different tangent or
bristle-noise coordinate than the build this PR measures against. Adding the
half in `f64` is exact for every window coordinate, so the helper matches
`f32::round` on that boundary again and the pixels are main's.

`rounding_matches_the_standard_library` now covers both half-integer
boundaries and the two `f32` steps either side of every integer a tile
coordinate can reach; it fails on the `f32` form at `0.49999997`.
This commit is contained in:
ebosido444-ai
2026-10-22 22:33:35 +03:00
parent c58c0412fb
commit f8f2df8c95
+17 -3
View File
@@ -31,11 +31,13 @@ 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 half is added in `f64`, where the sum is exact for
/// every `|v| < 2^22` (all window coordinates): in `f32` the largest value below `0.5` plus `0.5`
/// rounds up to `1.0`, which would return `1` where `f32::round` returns `0`. Larger magnitudes
/// saturate like the cast the caller used to perform.
#[inline]
fn round_i32(v: f32) -> i32 {
(v + 0.5f32.copysign(v)) as i32
(f64::from(v) + 0.5f64.copysign(f64::from(v))) as i32
}
/// [`round_i32`] clamped into `0..n`.
@@ -231,6 +233,18 @@ mod tests {
}
v += 0.25;
}
// The half-integer boundaries are where a `f32` add of the half rounds across the value
// `f32::round` keeps below: `0x3eff_ffff` is the largest `f32` under `0.5`.
for v in [0.5f32, -0.5, f32::from_bits(0x3eff_ffff), f32::from_bits(0xbeff_ffff)] {
assert_eq!(round_i32(v), v.round() as i32, "round_i32({v})");
}
// Every value one and two steps either side of an integer the LIC coordinates can reach.
for k in -40i32..=2100 {
let f = k as f32;
for v in [f.next_down().next_down(), f.next_down(), f, f.next_up()] {
assert_eq!(round_i32(v), v.round() as i32, "round_i32({v}) near {k}");
}
}
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);