mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
craft-texture: keep bilinear samples of finite texels finite
The four weighted texels are summed in f64 and a finite sum is clamped to the f32 range. Rounded weights can add up to slightly more than 1, which made a constant image of f32::MAX sample to infinity at some coordinates. Infinite texels still give an infinite or NaN sample. The crate docs and README now limit the OutOfMemory guarantee of the try_ forms to the pixel buffer, and name the Arc and the try_resize resampler as allocations that are not fallible.
This commit is contained in:
1 parent
261e7a4c5c
commit
316db97052
2 files changed
+27
-11
No files matched your search
@@ -12,9 +12,11 @@ network I/O and has no `unsafe` code.
|
||||
A plane is never larger than `isize::MAX` bytes (2 GiB minus one byte on `wasm32`), even with
|
||||
raised limits: the `try_` forms return `ImageError::TooLarge` for a larger one, and `map`,
|
||||
`to_color` and `to_kind` clamp each side to `1..=MAX_SIDE` and map the top-left part instead.
|
||||
`try_map`, `try_to_kind`, `try_resize` and `try_to_rgba8_display` reserve their memory first
|
||||
and return `ImageError::OutOfMemory` when that fails. The forms without `try_` allocate like
|
||||
`Vec`, which ends the process when memory runs out.
|
||||
`try_map`, `try_to_kind`, `try_resize` and `try_to_rgba8_display` reserve their pixel buffer
|
||||
first and return `ImageError::OutOfMemory` when that reservation fails. They still make small
|
||||
allocations that are not fallible: the `Arc` around a new image's buffer, and the boxed
|
||||
resampler of `try_resize`. The forms without `try_` allocate like `Vec`. A failed infallible
|
||||
allocation ends the process.
|
||||
- **Sampling.** `sample` (bilinear) and `sample_nearest` take normalized `(u, v)` with `(0, 0)` at
|
||||
the upper-left corner and `v` increasing downward, as glTF 2.0 section 3.8.3 defines them
|
||||
(<https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html>). `tile` picks repeat (glTF
|
||||
@@ -22,7 +24,8 @@ network I/O and has no `unsafe` code.
|
||||
and the bilinear weights follow the Vulkan specification, sections 20.4.1, 20.6.1 and 20.6.3
|
||||
(<https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#textures-texel-filtering>).
|
||||
Repeat keeps its period at any finite coordinate. A NaN or infinite coordinate gives
|
||||
`[0, 0, 0, 1]`.
|
||||
`[0, 0, 0, 1]`. Bilinear sums are taken in `f64` and clamped to the `f32` range, so finite
|
||||
texels give a finite sample.
|
||||
- **Other operations.** `get` reads one texel, `map`, `to_gray`, `to_color` and `to_kind` convert,
|
||||
`resize` box-averages whole-factor shrinks and samples bilinearly otherwise, and
|
||||
`to_rgba8_display` encodes 8-bit sRGB RGBA. `linear_to_srgb`, `srgb_to_linear` and `luminance`
|
||||
|
||||
@@ -19,10 +19,13 @@
|
||||
//! 2 GiB minus one byte on 32-bit targets such as `wasm32`. The `try_` forms return
|
||||
//! [`ImageError::TooLarge`] for a plane over that bound, also when the caller raised the limits
|
||||
//! past it. The constructors without `try_` clamp each side to `1..=MAX_SIDE`, and [`Image::map`]
|
||||
//! clamps the same way when its result would be over the bound. Every operation that allocates has
|
||||
//! a `try_` form that reserves its memory first and returns [`ImageError::OutOfMemory`] when that
|
||||
//! fails; the forms without `try_` allocate like `Vec`, which ends the process when memory runs
|
||||
//! out. Rows are generated on several threads only with the `parallel` feature.
|
||||
//! clamps the same way when its result would be over the bound. Every operation that allocates a
|
||||
//! pixel buffer has a `try_` form that reserves that buffer first and returns
|
||||
//! [`ImageError::OutOfMemory`] when the reservation fails. The `try_` forms still make small
|
||||
//! allocations that are not fallible: the `Arc` that holds a new image's buffer, and the boxed
|
||||
//! resampler of [`Image::try_resize`]. The forms without `try_` allocate like `Vec`. A failed
|
||||
//! infallible allocation ends the process. Rows are generated on several threads only with the
|
||||
//! `parallel` feature.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
@@ -343,17 +346,20 @@ impl Image {
|
||||
let (tx, ty) = (fx - x0, fy - y0);
|
||||
let (x0, y0) = (x0 as i64, y0 as i64);
|
||||
let taps = [((1.0 - tx) * (1.0 - ty), x0, y0), (tx * (1.0 - ty), x0 + 1, y0), ((1.0 - tx) * ty, x0, y0 + 1), (tx * ty, x0 + 1, y0 + 1)];
|
||||
let mut o = [0.0; 4];
|
||||
// Summed in f64: the rounded weights can add up to slightly more than 1, which would
|
||||
// overflow a finite HDR value near `f32::MAX` in f32. A finite sum is clamped to the f32
|
||||
// range; an infinite texel still gives an infinite or NaN sample.
|
||||
let mut o = [0.0f64; 4];
|
||||
for (weight, x, y) in taps {
|
||||
if weight == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let p = self.get(x, y, tile);
|
||||
for (o, p) in o.iter_mut().zip(p) {
|
||||
*o += weight * p;
|
||||
*o += f64::from(weight) * f64::from(p);
|
||||
}
|
||||
}
|
||||
o
|
||||
o.map(|v| if v.is_finite() { v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32 } else { v as f32 })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -724,6 +730,13 @@ mod tests {
|
||||
assert_eq!(img.sample(0.25, 0.5, false)[0], 1.0);
|
||||
let Ok(hdr) = Image::from_data(2, 1, Kind::Gray, vec![f32::MAX, -f32::MAX], &l) else { panic!("valid 2 x 1 image") };
|
||||
assert_eq!(hdr.sample(0.5, 0.5, false)[0], 0.0);
|
||||
// Rounded weights can sum past 1; a constant image of the largest float stays finite.
|
||||
let max = Image::filled(2, 2, Kind::Gray, [f32::MAX; 4]);
|
||||
let t = 0.5 + 2.0_f32.powi(-24);
|
||||
for tile in [false, true] {
|
||||
assert!(max.sample(0.250_012_5, t, tile)[0].is_finite(), "tile = {tile}");
|
||||
assert_eq!(max.sample(t, t, tile)[0], f32::MAX, "tile = {tile}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user