Skip to main content

primase_foundations/
sampling.rs

1//! Sampling a decoded texture: each policy the caller's, stated. Bilinear filtering between texel centres (a texel
2//! `i` covering `[i, i + 1)`, `uv` scaled by the level's size, minus a half), each axis's address mode (a border reads
3//! the colour the caller states, [`Addressing`]), the colour's
4//! transfer before filtering, at a level the caller picks. Two precisions, each a fixed order of operations: f64
5//! ([`Levels::bilinear`]) and f32 over texels decoded once ([`Decoded::bilinear`]). The texel a coordinate falls in
6//! ([`Levels::point`], [`Decoded::point`]). A level of detail from a sample's footprint ([`footprint_lod`]), and the
7//! two levels around it mixed ([`Levels::trilinear`]) or the nearest one taken ([`Levels::nearest_level`]).
8//!
9//! [`Decoded`] is one level of float texels, 2D or 3D: an RGBA8 level decoded once ([`Levels::decoded`]), or texels a
10//! caller holds as floats ([`Decoded::from_texels`], [`Decoded::volume`]). A volume filters linearly between the eight
11//! texels about a coordinate ([`Decoded::linear_3d`]) or takes the one it falls in ([`Decoded::point_3d`]).
12
13use crate::color::srgb8_to_linear;
14
15/// Why a sample or a texture's levels are refused.
16#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
17pub enum Error {
18    /// The texture or coordinate is invalid; the message says how.
19    #[error("invalid image: {0}")]
20    Invalid(String),
21}
22
23/// A result with this module's [`Error`].
24pub type Result<T> = std::result::Result<T, Error>;
25
26/// Where a texel index outside the texture reads (the address modes of Direct3D's `D3D11_TEXTURE_ADDRESS_MODE`).
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Address {
29    /// Repeats the texture.
30    Wrap,
31    /// The edge texel.
32    Clamp,
33    /// Repeats the texture, every other copy mirrored.
34    Mirror,
35    /// Mirrored once about the origin, then the edge texel.
36    MirrorOnce,
37    /// No texel: an index outside the texture reads the border colour ([`Addressing::border`]).
38    Border,
39}
40
41impl Address {
42    /// The texel index `i` reads along an axis of `size` texels, or none where it is outside a border axis.
43    fn index(self, i: i64, size: u32) -> Option<usize> {
44        let n = i64::from(size.max(1));
45        let (last, period) = (n.saturating_sub(1), n.saturating_mul(2));
46        let at = match self {
47            Address::Wrap => i.rem_euclid(n),
48            Address::Clamp => i.clamp(0, last),
49            // texel -1 is texel 0 mirrored, -2 is 1, ...: the pattern 0 1 .. n-1 n-1 .. 0 repeats every 2n texels
50            Address::Mirror => {
51                let m = i.rem_euclid(period);
52                if m < n {
53                    m
54                } else {
55                    period.saturating_sub(1).saturating_sub(m)
56                }
57            }
58            // mirrored once about the origin (-1 reads 0, -2 reads 1, ...), then the edge texel
59            Address::MirrorOnce => (if i < 0 { (-1i64).saturating_sub(i) } else { i }).clamp(0, last),
60            Address::Border if (0..n).contains(&i) => i,
61            Address::Border => return None,
62        };
63        // every arm lands in 0..n, and n is a u32
64        Some(usize::try_from(at).unwrap_or_default())
65    }
66}
67
68/// How a sample addresses a texture: each axis's [`Address`], and the colour an index outside the texture reads along
69/// a [`Address::Border`] axis. An array of addresses is addressing with no border colour (`[Address::Clamp; 2].into()`),
70/// so every filter takes either.
71#[derive(Clone, Copy, Debug, PartialEq)]
72pub struct Addressing<const N: usize> {
73    /// The address mode of each axis (u, v, then w).
74    pub axes: [Address; N],
75    /// The colour (red, green, blue, alpha) a border reads: a value of the filtered domain, taken as it is (the
76    /// [`Transfer`] decodes stored texels, not this). A sample with a border axis and no colour is refused.
77    pub border: Option<[f32; 4]>,
78}
79
80impl<const N: usize> From<[Address; N]> for Addressing<N> {
81    fn from(axes: [Address; N]) -> Addressing<N> {
82        Addressing { axes, border: None }
83    }
84}
85
86impl<const N: usize> Addressing<N> {
87    /// `axes`, a border reading `border`.
88    pub fn bordered(axes: [Address; N], border: [f32; 4]) -> Addressing<N> {
89        Addressing {
90            axes,
91            border: Some(border),
92        }
93    }
94
95    /// The colour a border reads, checked once per sample: a border axis with no colour, or a colour that isn't finite,
96    /// is refused. Zero where no axis is a border (no index then reads it).
97    fn border_colour(&self) -> Result<[f32; 4]> {
98        match self.border {
99            Some(c) if c.iter().any(|v| !v.is_finite()) => Err(Error::Invalid(format!(
100                "sampled texture: border colour {c:?} is not finite"
101            ))),
102            Some(c) => Ok(c),
103            None if self.axes.contains(&Address::Border) => Err(Error::Invalid(
104                "sampled texture: border addressing with no border colour".into(),
105            )),
106            None => Ok([0.0; 4]),
107        }
108    }
109
110    /// The texel indices `at` reads in a texture of `size` texels along each axis, or none where one is outside a
111    /// border axis.
112    fn indices(&self, at: [i64; N], size: [u32; N]) -> Option<[usize; N]> {
113        let mut out = [0usize; N];
114        for (o, ((a, i), n)) in out.iter_mut().zip(self.axes.iter().zip(at).zip(size)) {
115            *o = a.index(i, n)?;
116        }
117        Some(out)
118    }
119}
120
121/// A texel channel's value before filtering.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum Transfer {
124    /// `x / 255`.
125    Stored,
126    /// Red, green and blue decoded from sRGB to linear (as a GPU's sRGB view), alpha `x / 255`.
127    SrgbColour,
128}
129
130impl Transfer {
131    /// Channel `c` (0-3: red, green, blue, alpha) of a texel stored as `byte`, before filtering, in f64.
132    pub fn channel(self, c: usize, byte: u8) -> f64 {
133        match self {
134            Transfer::SrgbColour if c < 3 => srgb8_to_linear(byte),
135            _ => f64::from(byte) / 255.0,
136        }
137    }
138}
139
140/// An RGBA8 texture's mip levels, each stored row by row, four bytes a texel. Level `n` is the base size halved `n`
141/// times (at least 1); [`Levels::size`] gives it.
142#[derive(Clone, Debug)]
143pub struct Levels {
144    /// The base level's width in texels.
145    pub width: u32,
146    /// The base level's height in texels.
147    pub height: u32,
148    /// The RGBA8 bytes of each level, base level first. The fields are public, so the sampling methods check the
149    /// level they read: one that is missing, or does not hold exactly its [`Levels::size`] in texels, is refused.
150    pub levels: Vec<Vec<u8>>,
151}
152
153impl Levels {
154    /// One level of RGBA8 texels.
155    pub fn one(width: u32, height: u32, rgba: Vec<u8>) -> Result<Levels> {
156        let count = (width as usize)
157            .checked_mul(height as usize)
158            .filter(|&n| n > 0)
159            .ok_or_else(|| Error::Invalid("sampled texture: invalid dimensions".into()))?;
160        if count.checked_mul(4) != Some(rgba.len()) {
161            return Err(Error::Invalid("sampled texture: RGBA length mismatch".into()));
162        }
163        Ok(Levels {
164            width,
165            height,
166            levels: vec![rgba],
167        })
168    }
169
170    /// Each level's rows in reverse order, for a texture stored bottom row first when row 0 is wanted at v = 0:
171    /// addressing then applies about that texture's own origin. A level that isn't its size's whole rows is refused.
172    pub fn rows_reversed(mut self) -> Result<Levels> {
173        for i in 0..self.levels.len() {
174            let row = self.level(i)?.row_bytes;
175            let Some(level) = self.levels.get_mut(i) else {
176                continue;
177            };
178            *level = level.chunks_exact(row).rev().flatten().copied().collect();
179        }
180        Ok(self)
181    }
182
183    /// Level `level`'s size: the base halved per level, at least 1.
184    pub fn size(&self, level: usize) -> (u32, u32) {
185        let s = u32::try_from(level).unwrap_or(31);
186        (
187            (self.width >> s.min(31)).max(1),
188            (self.height >> s.min(31)).max(1),
189        )
190    }
191
192    /// Level `level`'s bytes if it is stored and holds exactly its size's texels, else refused.
193    fn level(&self, level: usize) -> Result<Level<'_>> {
194        let (w, h) = self.size(level);
195        let bytes = self.levels.get(level).ok_or_else(|| {
196            Error::Invalid(format!(
197                "sampled texture: level {level} requested, expected one of the {} stored levels",
198                self.levels.len()
199            ))
200        })?;
201        let row_bytes = (w as usize).checked_mul(4);
202        let want = row_bytes.and_then(|r| r.checked_mul(h as usize));
203        match (row_bytes, want) {
204            (Some(row_bytes), Some(want)) if want == bytes.len() && want > 0 => Ok(Level {
205                bytes,
206                row_bytes,
207                width: w,
208                height: h,
209            }),
210            _ => Err(Error::Invalid(format!(
211                "sampled texture: level {level} holds {} bytes, expected {h} rows of {w} RGBA8 texels",
212                bytes.len()
213            ))),
214        }
215    }
216
217    /// The texel at (x, y) of level `level`, addressed by `address`. A level that is not stored, or does not hold
218    /// its size's bytes, is refused, as is a coordinate outside a [`Address::Border`] axis (it reads no stored texel).
219    pub fn texel(&self, level: usize, x: i64, y: i64, address: [Address; 2]) -> Result<[u8; 4]> {
220        self.level(level)?
221            .texel(x, y, &Addressing::from(address))?
222            .ok_or_else(|| {
223                Error::Invalid(format!(
224                    "sampled texture: texel ({x}, {y}) is outside a border axis"
225                ))
226            })
227    }
228
229    /// Bilinear at `uv` of level `level`, in f64: weights `(1 - tx)(1 - ty)`, `tx (1 - ty)`, `(1 - tx) ty`, `tx ty`
230    /// summed in that order, a texel outside a border axis the border colour. A coordinate that isn't finite or past an
231    /// i32's range is refused, as is a border axis with no colour ([`Addressing::border`]).
232    pub fn bilinear(
233        &self,
234        level: usize,
235        uv: [f64; 2],
236        address: impl Into<Addressing<2>>,
237        transfer: Transfer,
238    ) -> Result<[f64; 4]> {
239        let address: Addressing<2> = address.into();
240        let border = address.border_colour()?;
241        let (w, h) = self.size(level);
242        let level = self.level(level)?;
243        let (x, y) = texel_coordinate([uv[0] * f64::from(w) - 0.5, uv[1] * f64::from(h) - 0.5])?;
244        let (fx, fy) = (x.floor(), y.floor());
245        let (tx, ty) = (x - fx, y - fy);
246        let (ix, iy) = (floor_f64(fx), floor_f64(fy));
247        let mut out = [0.0; 4];
248        for (dx, dy, wt) in [
249            (0, 0, (1.0 - tx) * (1.0 - ty)),
250            (1, 0, tx * (1.0 - ty)),
251            (0, 1, (1.0 - tx) * ty),
252            (1, 1, tx * ty),
253        ] {
254            let t = level.texel(ix.saturating_add(dx), iy.saturating_add(dy), &address)?;
255            for (c, o) in out.iter_mut().enumerate() {
256                *o += wt * texel_channel(t, c, transfer, border);
257            }
258        }
259        Ok(out)
260    }
261
262    /// The texel of level `level` that `uv` falls in (texel `i` covering `[i, i + 1)` after scaling by the level's
263    /// size), in f64: point filtering; outside a border axis, the border colour. A coordinate that isn't finite or past
264    /// an i32's range is refused, as is a border axis with no colour.
265    pub fn point(
266        &self,
267        level: usize,
268        uv: [f64; 2],
269        address: impl Into<Addressing<2>>,
270        transfer: Transfer,
271    ) -> Result<[f64; 4]> {
272        let address: Addressing<2> = address.into();
273        let border = address.border_colour()?;
274        let (w, h) = self.size(level);
275        let level = self.level(level)?;
276        let (x, y) = texel_coordinate([uv[0] * f64::from(w), uv[1] * f64::from(h)])?;
277        let t = level.texel(floor_f64(x), floor_f64(y), &address)?;
278        Ok(std::array::from_fn(|c| texel_channel(t, c, transfer, border)))
279    }
280
281    /// Level `level` decoded once to f32 by `transfer`, for [`Decoded::bilinear`]. A level that is not stored, or does
282    /// not hold its size's bytes, is refused.
283    pub fn decoded(&self, level: usize, transfer: Transfer) -> Result<Decoded> {
284        let l = self.level(level)?;
285        let texels = l
286            .bytes
287            .as_chunks::<4>()
288            .0
289            .iter()
290            .map(|p| {
291                let mut texel = [0f32; 4];
292                for (c, (out, &byte)) in texel.iter_mut().zip(p).enumerate() {
293                    *out = match transfer {
294                        Transfer::SrgbColour if c < 3 => to_f32(srgb8_to_linear(byte)),
295                        _ => f32::from(byte) / 255.0,
296                    };
297                }
298                texel
299            })
300            .collect();
301        Ok(Decoded {
302            width: l.width,
303            height: l.height,
304            depth: 1,
305            texels,
306        })
307    }
308}
309
310/// One stored level that holds exactly its size's texels (checked by [`Levels::level`]).
311struct Level<'a> {
312    bytes: &'a [u8],
313    row_bytes: usize,
314    width: u32,
315    height: u32,
316}
317
318/// Channel `c` of a stored texel through `transfer`, or of the border colour where the texel is outside a border axis.
319fn texel_channel(texel: Option<[u8; 4]>, c: usize, transfer: Transfer, border: [f32; 4]) -> f64 {
320    match texel {
321        Some(t) => transfer.channel(c, t.get(c).copied().unwrap_or_default()),
322        None => f64::from(border.get(c).copied().unwrap_or_default()),
323    }
324}
325
326impl Level<'_> {
327    /// The stored texel at addressed (x, y), or none where it is outside a border axis.
328    fn texel(&self, x: i64, y: i64, address: &Addressing<2>) -> Result<Option<[u8; 4]>> {
329        let Some([x, y]) = address.indices([x, y], [self.width, self.height]) else {
330            return Ok(None);
331        };
332        let i = y
333            .checked_mul(self.row_bytes)
334            .and_then(|row| row.checked_add(x.checked_mul(4)?));
335        i.and_then(|i| self.bytes.get(i..)?.first_chunk::<4>())
336            .copied()
337            .map(Some)
338            .ok_or_else(|| {
339                Error::Invalid(format!(
340                    "sampled texture: texel ({x}, {y}) outside the level's {} bytes",
341                    self.bytes.len()
342                ))
343            })
344    }
345}
346
347/// A texel coordinate `[x, y]` that is finite and within an i32's range, else refused.
348fn texel_coordinate([x, y]: [f64; 2]) -> Result<(f64, f64)> {
349    if !x.is_finite() || !y.is_finite() || x.abs() >= 2147483520.0 || y.abs() >= 2147483520.0 {
350        return Err(Error::Invalid(
351            "sampled texture: nonfinite or excessive texel coordinate".into(),
352        ));
353    }
354    Ok((x, y))
355}
356
357/// The level of detail of a footprint: the UV's screen derivatives `gradients` (d/dx, then d/dy) in texels of a
358/// `width` x `height` base level, the longer one's base-2 logarithm (Direct3D's isotropic rule), plus `bias`. A
359/// footprint of no extent gives negative infinity (the finest level, whatever the bias).
360pub fn footprint_lod(width: u32, height: u32, gradients: [[f64; 2]; 2], bias: f64) -> f64 {
361    let texels = |d: [f64; 2]| (d[0] * f64::from(width)).hypot(d[1] * f64::from(height));
362    texels(gradients[0]).max(texels(gradients[1])).log2() + bias
363}
364
365impl Levels {
366    /// Trilinear at `uv`: the level of detail `lod` clamped to the stored levels, the bilinear values
367    /// ([`Levels::bilinear`]) of the level below it and the next mixed by its fraction (`a + (b - a) f`); a level
368    /// of detail that isn't a number, or a coordinate or addressing [`Levels::bilinear`] refuses, is refused.
369    pub fn trilinear(
370        &self,
371        uv: [f64; 2],
372        lod: f64,
373        address: impl Into<Addressing<2>>,
374        transfer: Transfer,
375    ) -> Result<[f64; 4]> {
376        let address: Addressing<2> = address.into();
377        if lod.is_nan() {
378            return Err(Error::Invalid(
379                "sampled texture: a level of detail that isn't a number".into(),
380            ));
381        }
382        let last = self.levels.len().saturating_sub(1);
383        let lod = lod.clamp(0.0, last as f64);
384        let below = lod.floor();
385        let (l0, f) = (level_number(below), lod - below);
386        let a = self.bilinear(l0, uv, address, transfer)?;
387        if f == 0.0 {
388            return Ok(a);
389        }
390        let b = self.bilinear(l0.saturating_add(1).min(last), uv, address, transfer)?;
391        let mut mixed = [0.0; 4];
392        for (m, (a, b)) in mixed.iter_mut().zip(a.into_iter().zip(b)) {
393            *m = a + (b - a) * f;
394        }
395        Ok(mixed)
396    }
397
398    /// The stored level nearest the level of detail `lod`, as a point mip filter picks it: `floor(lod + 0.5)` (a half
399    /// rounds up), clamped to the stored levels. A level of detail that isn't a number is refused.
400    pub fn nearest_level(&self, lod: f64) -> Result<usize> {
401        if lod.is_nan() {
402            return Err(Error::Invalid(
403                "sampled texture: a level of detail that isn't a number".into(),
404            ));
405        }
406        let last = self.levels.len().saturating_sub(1);
407        Ok(level_number((lod + 0.5).floor().clamp(0.0, last as f64)))
408    }
409}
410
411/// How finely a bilinear filter resolves where a sample falls between texels.
412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub enum Subtexel {
414    /// The position in f32, as computed.
415    Exact,
416    /// Direct3D 11's filtering: the texel coordinate (after scaling by the texture's size) truncated to 8 fractional
417    /// bits, 256 subdivisions of a texel, before the weights are taken. The D3D11.3 functional specification requires
418    /// at least 8 bits of sub-texel precision (7.18.16.1, Texture Addressing and LOD Precision;
419    /// `D3D11_SUBTEXEL_FRACTIONAL_BIT_COUNT` = 8). Microsoft's WARP software rasterizer truncates at 8, as measured
420    /// against it.
421    D3d11,
422}
423
424impl Subtexel {
425    /// `x` resolved to this precision: unchanged, or truncated to 1/256.
426    fn resolve(self, x: f32) -> f32 {
427        match self {
428            Subtexel::Exact => x,
429            Subtexel::D3d11 => (x * 256.).floor() / 256.,
430        }
431    }
432}
433
434/// One level's texels in f32, their transfer applied: a 2D level (`depth` 1) or a volume, row by row and slice by
435/// slice.
436#[derive(Clone, Debug)]
437pub struct Decoded {
438    width: u32,
439    height: u32,
440    depth: u32,
441    texels: Vec<[f32; 4]>,
442}
443
444/// A texel coordinate in f32 that is finite and within an i32's range, else refused.
445fn texel_coordinate_f32(x: f32) -> Result<f32> {
446    if !x.is_finite() || x.abs() >= 2147483520.0 {
447        return Err(Error::Invalid(
448            "sampled texture: nonfinite or excessive texel coordinate".into(),
449        ));
450    }
451    Ok(x)
452}
453
454impl Decoded {
455    /// A 2D level of `width` x `height` float texels, row 0 first, as the caller holds them (no transfer: the values
456    /// are filtered as they are). No texel, or a count other than the size's, is refused.
457    pub fn from_texels(width: u32, height: u32, texels: Vec<[f32; 4]>) -> Result<Decoded> {
458        Decoded::volume(width, height, 1, texels)
459    }
460
461    /// A volume of `width` x `height` x `depth` float texels, slice 0 first and each slice row 0 first. No texel, or a
462    /// count other than the size's, is refused.
463    pub fn volume(width: u32, height: u32, depth: u32, texels: Vec<[f32; 4]>) -> Result<Decoded> {
464        let count = (width as usize)
465            .checked_mul(height as usize)
466            .and_then(|n| n.checked_mul(depth as usize))
467            .filter(|&n| n > 0)
468            .ok_or_else(|| Error::Invalid("sampled texture: invalid dimensions".into()))?;
469        if texels.len() != count {
470            return Err(Error::Invalid(format!(
471                "sampled texture: {} texels for {width} x {height} x {depth}",
472                texels.len()
473            )));
474        }
475        Ok(Decoded {
476            width,
477            height,
478            depth,
479            texels,
480        })
481    }
482
483    /// The level's `[width, height]` in texels.
484    pub fn dimensions(&self) -> [usize; 2] {
485        [self.width as usize, self.height as usize]
486    }
487    /// The slices of a volume: 1 for a 2D level.
488    pub fn depth(&self) -> usize {
489        self.depth as usize
490    }
491    /// The memory the decoded texels take, in bytes (16 a texel).
492    pub fn decoded_bytes(&self) -> usize {
493        self.texels.len().saturating_mul(16)
494    }
495
496    /// The texel at (x, y, z) if the level holds one there.
497    fn get(&self, x: usize, y: usize, z: usize) -> Option<[f32; 4]> {
498        let index = z
499            .checked_mul(self.height as usize)?
500            .checked_add(y)?
501            .checked_mul(self.width as usize)?
502            .checked_add(x)?;
503        self.texels.get(index).copied()
504    }
505
506    /// The texel at (x, y, z), if the level holds one there: no addressing, a load's view of the level.
507    pub fn texel(&self, x: i64, y: i64, z: i64) -> Option<[f32; 4]> {
508        let inside = |i: i64, n: u32| usize::try_from(i).ok().filter(|&i| i < n as usize);
509        self.get(
510            inside(x, self.width)?,
511            inside(y, self.height)?,
512            inside(z, self.depth)?,
513        )
514    }
515
516    /// The texel at addressed (x, y, z): addressing reduces each into the level, so a miss means the texels do not
517    /// match the size, which the constructors rule out.
518    fn at(&self, x: usize, y: usize, z: usize) -> Result<[f32; 4]> {
519        self.get(x, y, z).ok_or_else(|| {
520            Error::Invalid(format!(
521                "sampled texture: texel ({x}, {y}, {z}) outside the {} texels of a {} x {} x {} level",
522                self.texels.len(),
523                self.width,
524                self.height,
525                self.depth
526            ))
527        })
528    }
529
530    /// A 2D sample of a volume is refused: which slice it reads is not the level's to say.
531    fn plane(&self) -> Result<()> {
532        if self.depth == 1 {
533            Ok(())
534        } else {
535            Err(Error::Invalid(format!(
536                "sampled texture: a 2D sample of a volume of {} slices",
537                self.depth
538            )))
539        }
540    }
541
542    /// Bilinear at `uv`, in f32 (the coordinate, the weights `(1 - t)` and `t` per axis, and the sum over the four
543    /// texels row by row), a texel outside a border axis the border colour; a coordinate that isn't finite or past an
544    /// i32's range is refused, as is a border axis with no colour.
545    pub fn bilinear(&self, uv: [f32; 2], address: impl Into<Addressing<2>>) -> Result<[f32; 4]> {
546        self.bilinear_at(uv, address, Subtexel::Exact)
547    }
548    /// [`Decoded::bilinear`] with the texel coordinate resolved to `subtexel` precision before the weights are taken.
549    pub fn bilinear_at(
550        &self,
551        uv: [f32; 2],
552        address: impl Into<Addressing<2>>,
553        subtexel: Subtexel,
554    ) -> Result<[f32; 4]> {
555        let address: Addressing<2> = address.into();
556        let border = address.border_colour()?;
557        self.plane()?;
558        let [u, v] = uv;
559        let x = texel_coordinate_f32(subtexel.resolve(u * self.width as f32 - 0.5))?;
560        let y = texel_coordinate_f32(subtexel.resolve(v * self.height as f32 - 0.5))?;
561        let (ix, iy) = (floor_f32(x), floor_f32(y));
562        let (tx, ty) = (x - ix as f32, y - iy as f32);
563        let mut result = [0.; 4];
564        for dy in 0..2 {
565            for dx in 0..2 {
566                let weight = (if dx == 1 { tx } else { 1. - tx }) * (if dy == 1 { ty } else { 1. - ty });
567                let texel = match address.indices(
568                    [ix.saturating_add(dx), iy.saturating_add(dy)],
569                    [self.width, self.height],
570                ) {
571                    Some([xx, yy]) => self.at(xx, yy, 0)?,
572                    None => border,
573                };
574                for (out, t) in result.iter_mut().zip(texel) {
575                    *out += t * weight;
576                }
577            }
578        }
579        Ok(result)
580    }
581
582    /// The texel `uv` falls in (texel `i` covering `[i, i + 1)` after scaling by the size), addressed, in f32: point
583    /// filtering; outside a border axis, the border colour. A coordinate that isn't finite or past an i32's range is
584    /// refused, as is a border axis with no colour.
585    pub fn point(&self, uv: [f32; 2], address: impl Into<Addressing<2>>) -> Result<[f32; 4]> {
586        let address: Addressing<2> = address.into();
587        let border = address.border_colour()?;
588        self.plane()?;
589        let [u, v] = uv;
590        let x = texel_coordinate_f32(u * self.width as f32)?;
591        let y = texel_coordinate_f32(v * self.height as f32)?;
592        match address.indices([floor_f32(x), floor_f32(y)], [self.width, self.height]) {
593            Some([xx, yy]) => self.at(xx, yy, 0),
594            None => Ok(border),
595        }
596    }
597
598    /// The texel of a volume (or a 2D level, one slice) that `uvw` falls in, addressed along each axis, in f32; outside
599    /// a border axis, the border colour.
600    pub fn point_3d(&self, uvw: [f32; 3], address: impl Into<Addressing<3>>) -> Result<[f32; 4]> {
601        let address: Addressing<3> = address.into();
602        let border = address.border_colour()?;
603        let [u, v, w] = uvw;
604        let axis = |c: f32, n: u32| -> Result<i64> { Ok(floor_f32(texel_coordinate_f32(c * n as f32)?)) };
605        let at = [axis(u, self.width)?, axis(v, self.height)?, axis(w, self.depth)?];
606        match address.indices(at, [self.width, self.height, self.depth]) {
607            Some([x, y, z]) => self.at(x, y, z),
608            None => Ok(border),
609        }
610    }
611
612    /// Linear between the eight texels about `uvw` in a volume, in f32: the coordinate per axis (scaled by the size,
613    /// minus a half, resolved to `subtexel` precision), the weights `(1 - t)` and `t` per axis multiplied x, then y,
614    /// then z, and the sum over the texels slice by slice, row by row. A coordinate that isn't finite or past an i32's
615    /// range is refused.
616    pub fn linear_3d(
617        &self,
618        uvw: [f32; 3],
619        address: impl Into<Addressing<3>>,
620        subtexel: Subtexel,
621    ) -> Result<[f32; 4]> {
622        let address: Addressing<3> = address.into();
623        let border = address.border_colour()?;
624        let [u, v, w] = uvw;
625        let (sw, sh, sd) = (self.width, self.height, self.depth);
626        let axis = |c: f32, n: u32| -> Result<(i64, f32)> {
627            let t = texel_coordinate_f32(subtexel.resolve(c * n as f32 - 0.5))?;
628            let base = floor_f32(t);
629            Ok((base, t - base as f32))
630        };
631        let ((bx, tx), (by, ty), (bz, tz)) = (axis(u, sw)?, axis(v, sh)?, axis(w, sd)?);
632        let weight = |d: i64, t: f32| if d == 1 { t } else { 1. - t };
633        let mut result = [0.; 4];
634        for dz in 0..2 {
635            for dy in 0..2 {
636                for dx in 0..2 {
637                    let texel = match address.indices(
638                        [
639                            bx.saturating_add(dx),
640                            by.saturating_add(dy),
641                            bz.saturating_add(dz),
642                        ],
643                        [sw, sh, sd],
644                    ) {
645                        Some([x, y, z]) => self.at(x, y, z)?,
646                        None => border,
647                    };
648                    let wt = weight(dx, tx) * weight(dy, ty) * weight(dz, tz);
649                    for (out, t) in result.iter_mut().zip(texel) {
650                        *out += t * wt;
651                    }
652                }
653            }
654        }
655        Ok(result)
656    }
657}
658
659/// The floor of an f32 coordinate that [`texel_coordinate_f32`] has checked to lie within an i32's range.
660#[allow(
661    clippy::cast_possible_truncation,
662    reason = "the caller checked the coordinate is finite and within an i32's range"
663)]
664fn floor_f32(x: f32) -> i64 {
665    x.floor() as i64
666}
667
668/// The floor of an f64 coordinate that [`texel_coordinate`] has checked to lie within an i32's range.
669#[allow(
670    clippy::cast_possible_truncation,
671    reason = "the caller checked the coordinate is finite and within an i32's range"
672)]
673fn floor_f64(x: f64) -> i64 {
674    x.floor() as i64
675}
676
677/// A decoded sRGB value narrowed to the f32 a decoded texel holds.
678#[allow(
679    clippy::cast_possible_truncation,
680    reason = "rounding the f64 curve value to the f32 texel is the documented decode"
681)]
682fn to_f32(x: f64) -> f32 {
683    x as f32
684}
685
686/// A level number from a level of detail already clamped to `0..=last`.
687#[allow(
688    clippy::cast_possible_truncation,
689    reason = "the caller clamped the value to a stored level number"
690)]
691fn level_number(lod: f64) -> usize {
692    lod as usize
693}
694
695#[cfg(test)]
696#[allow(clippy::unwrap_used, reason = "test fixtures")]
697mod tests {
698    use super::*;
699
700    /// Texel indices out of range, worked by hand for four texels: repeat, mirrored repeat (-1 reads 0, 4 reads 3, the
701    /// pattern 0 1 2 3 3 2 1 0 repeating), clamp, and mirrored once about the origin then the edge.
702    #[test]
703    fn addressing_out_of_range() {
704        let read = |a: Address, from: i64, to: i64| -> Vec<usize> {
705            (from..=to).map(|i| a.index(i, 4).unwrap()).collect()
706        };
707        assert_eq!(read(Address::Wrap, -5, 5), [3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1]);
708        assert_eq!(
709            read(Address::Mirror, -5, 8),
710            [3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 2, 1, 0, 0]
711        );
712        assert_eq!(read(Address::Clamp, -2, 5), [0, 0, 0, 1, 2, 3, 3, 3]);
713        assert_eq!(
714            read(Address::MirrorOnce, -5, 5),
715            [3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 3]
716        );
717        // a border axis reads the texels inside, and no texel outside
718        let border: Vec<Option<usize>> = (-2..=5).map(|i| Address::Border.index(i, 4)).collect();
719        assert_eq!(
720            border,
721            [None, None, Some(0), Some(1), Some(2), Some(3), None, None]
722        );
723    }
724
725    /// Border addressing reads the stated colour outside the texture, in both precisions and every filter: a 2 x 1
726    /// texture, black then white, sampled past its left edge reads the border there; bilinear at the left edge mixes
727    /// half the border with half the black texel; inside, the texels. The colour is taken as it is (no transfer). A
728    /// border axis with no colour, or a colour that isn't finite, is refused; an array of addresses has none.
729    #[test]
730    fn border_addressing_reads_the_stated_colour() {
731        let l = Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255]).unwrap();
732        let axes = [Address::Border, Address::Clamp];
733        let (zero, one) = (
734            Addressing::bordered(axes, [0.0; 4]),
735            Addressing::bordered(axes, [1.0; 4]),
736        );
737        // point, past the left edge: the border; inside: the texel
738        assert_eq!(
739            l.point(0, [-0.25, 0.5], zero, Transfer::Stored).unwrap(),
740            [0.0; 4]
741        );
742        assert_eq!(l.point(0, [-0.25, 0.5], one, Transfer::Stored).unwrap(), [1.0; 4]);
743        assert_eq!(
744            l.point(0, [0.25, 0.5], one, Transfer::Stored).unwrap(),
745            [0.0, 0.0, 0.0, 1.0]
746        );
747        // bilinear at u = 0: half a texel left of the black texel's centre, half the border and half black
748        assert_eq!(
749            l.bilinear(0, [0.0, 0.5], one, Transfer::Stored).unwrap(),
750            [0.5, 0.5, 0.5, 1.0]
751        );
752        assert_eq!(
753            l.bilinear(0, [0.0, 0.5], zero, Transfer::Stored).unwrap(),
754            [0.0, 0.0, 0.0, 0.5]
755        );
756        // the colour is not decoded by the transfer
757        let grey = Addressing::bordered(axes, [0.5; 4]);
758        assert_eq!(
759            l.point(0, [-0.25, 0.5], grey, Transfer::SrgbColour).unwrap(),
760            [0.5; 4]
761        );
762        assert_eq!(
763            l.trilinear([-1.0, 0.5], 0.0, one, Transfer::Stored).unwrap(),
764            [1.0; 4]
765        );
766        // f32 texels, 2D and volumes
767        let d = l.decoded(0, Transfer::Stored).unwrap();
768        assert_eq!(d.point([1.5, 0.5], one).unwrap(), [1.0; 4]);
769        assert_eq!(d.bilinear([0.0, 0.5], one).unwrap(), [0.5, 0.5, 0.5, 1.0]);
770        assert_eq!(d.bilinear([0.0, 0.5], zero).unwrap(), [0.0, 0.0, 0.0, 0.5]);
771        let v = Decoded::volume(1, 1, 2, vec![[0.25; 4], [0.75; 4]]).unwrap();
772        let volume = Addressing::bordered([Address::Clamp, Address::Clamp, Address::Border], [1.0; 4]);
773        assert_eq!(v.point_3d([0.5, 0.5, 1.5], volume).unwrap(), [1.0; 4]);
774        assert_eq!(v.point_3d([0.5, 0.5, 0.25], volume).unwrap(), [0.25; 4]);
775        // halfway between the far slice's centre and the border beyond it
776        assert_eq!(
777            v.linear_3d([0.5, 0.5, 1.0], volume, Subtexel::Exact).unwrap(),
778            [0.875; 4]
779        );
780        // a border axis needs a colour, a finite one; the stored texel past a border is none
781        assert!(l.point(0, [0.25, 0.5], axes, Transfer::Stored).is_err());
782        assert!(d.bilinear([0.5, 0.5], axes).is_err());
783        let nan = Addressing::bordered(axes, [f32::NAN, 0.0, 0.0, 0.0]);
784        assert!(l.bilinear(0, [0.5, 0.5], nan, Transfer::Stored).is_err());
785        assert!(l.texel(0, -1, 0, axes).is_err());
786        assert_eq!(l.texel(0, 1, 0, axes).unwrap(), [255; 4]);
787        // an array of addresses is addressing with no border colour
788        assert_eq!(
789            Addressing::from([Address::Wrap; 2]),
790            Addressing {
791                axes: [Address::Wrap; 2],
792                border: None
793            }
794        );
795    }
796
797    /// Direct3D 11's sub-texel precision truncates the texel coordinate to 1/256: a sample 0.3 of the way from black to
798    /// white weighs white 76/256 (0.3 * 256 = 76.8, truncated), where the exact filter weighs it 0.3; on a 1/256 step
799    /// both agree.
800    #[test]
801    fn d3d11_subtexel_precision_truncates_to_eight_bits() {
802        let d = Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255])
803            .unwrap()
804            .decoded(0, Transfer::Stored)
805            .unwrap();
806        let clamp = [Address::Clamp; 2];
807        // texel coordinate 0.3: u = (0.3 + 0.5) / 2
808        let u = 0.4f32;
809        let exact = d.bilinear_at([u, 0.5], clamp, Subtexel::Exact).unwrap()[0];
810        let d3d = d.bilinear_at([u, 0.5], clamp, Subtexel::D3d11).unwrap()[0];
811        assert!((exact - 0.3).abs() < 1e-6, "{exact}");
812        assert_eq!(d3d, 76. / 256.);
813        let on_step = (64. / 256. + 0.5) / 2.;
814        assert_eq!(
815            d.bilinear_at([on_step, 0.5], clamp, Subtexel::D3d11).unwrap(),
816            d.bilinear_at([on_step, 0.5], clamp, Subtexel::Exact).unwrap()
817        );
818    }
819
820    /// A 2 x 1 texture, black then white: halfway between the texel centres is half white; at the left texel's centre
821    /// black; wrapped, the left edge blends the right texel in, clamped it doesn't; sRGB colour decoded before
822    /// filtering (a half of white's linear 1), alpha as stored.
823    #[test]
824    fn bilinear_between_texel_centres() {
825        let l = Levels::one(2, 1, vec![0, 0, 0, 0, 255, 255, 255, 255]).unwrap();
826        let wrap = [Address::Wrap; 2];
827        let clamp = [Address::Clamp; 2];
828        assert_eq!(
829            l.bilinear(0, [0.5, 0.5], wrap, Transfer::Stored).unwrap(),
830            [0.5; 4]
831        );
832        assert_eq!(
833            l.bilinear(0, [0.25, 0.5], wrap, Transfer::Stored).unwrap(),
834            [0.0; 4]
835        );
836        assert_eq!(
837            l.bilinear(0, [0.0, 0.5], wrap, Transfer::Stored).unwrap(),
838            [0.5; 4]
839        );
840        assert_eq!(
841            l.bilinear(0, [0.0, 0.5], clamp, Transfer::Stored).unwrap(),
842            [0.0; 4]
843        );
844        let d = l.decoded(0, Transfer::SrgbColour).unwrap();
845        let half = d.bilinear([0.5, 0.5], wrap).unwrap();
846        assert!(half.iter().all(|c| (c - 0.5).abs() < 1e-6), "{half:?}");
847        // a 50% grey texel: its sRGB colour decodes to linear 0.2158...; its alpha stays 128/255
848        let g = Levels::one(1, 1, vec![128, 128, 128, 128]).unwrap();
849        let c = g.bilinear(0, [0.5, 0.5], wrap, Transfer::SrgbColour).unwrap();
850        assert!(
851            (c[0] - crate::color::srgb_to_linear(128.0 / 255.0)).abs() < 1e-15
852                && (c[3] - 128.0 / 255.0).abs() < 1e-15
853        );
854    }
855
856    /// A footprint of one base texel is level 0, of four texels level 2 (the longer derivative counts, in texels of
857    /// each axis's size); trilinear between levels 0 (black) and 1 (white) gives the fraction, and clamps to the
858    /// stored levels.
859    #[test]
860    fn trilinear_at_a_footprint() {
861        assert_eq!(
862            footprint_lod(8, 4, [[1.0 / 8.0, 0.0], [0.0, 1.0 / 4.0]], 0.0),
863            0.0
864        );
865        assert_eq!(footprint_lod(8, 4, [[0.0, 0.0], [0.0, 1.0]], 0.0), 2.0);
866        assert_eq!(footprint_lod(8, 4, [[0.0; 2]; 2], 3.0), f64::NEG_INFINITY);
867        let l = Levels {
868            width: 2,
869            height: 2,
870            levels: vec![vec![0; 16], vec![255; 4]],
871        };
872        let wrap = [Address::Wrap; 2];
873        let at = |lod| l.trilinear([0.5, 0.5], lod, wrap, Transfer::Stored).unwrap()[0];
874        assert_eq!((at(-1.0), at(0.25), at(1.0), at(7.0)), (0.0, 0.25, 1.0, 1.0));
875        assert!(l.trilinear([0.5, 0.5], f64::NAN, wrap, Transfer::Stored).is_err());
876    }
877
878    /// Point filtering takes the texel a coordinate falls in, addressed, its transfer applied; the nearest level
879    /// rounds a half up and clamps to the stored levels; reversed rows put the bottom row at v = 0 in every level.
880    #[test]
881    fn point_filtering_nearest_levels_and_reversed_rows() {
882        // 2 x 2, rows top first: black, white / grey, white; then a 1 x 1 level
883        let l = Levels {
884            width: 2,
885            height: 2,
886            levels: vec![[[0; 4], [255; 4], [128; 4], [255; 4]].concat(), vec![64; 4]],
887        };
888        let (wrap, clamp) = ([Address::Wrap; 2], [Address::Clamp; 2]);
889        let at = |l: &Levels, uv: [f64; 2], a| l.point(0, uv, a, Transfer::Stored).unwrap()[0];
890        assert_eq!(at(&l, [0.25, 0.25], wrap), 0.0);
891        assert_eq!(at(&l, [0.5, 0.25], wrap), 1.0);
892        assert_eq!(at(&l, [0.25, 0.75], wrap), 128.0 / 255.0);
893        // past the left edge: wrapped to the right texel, clamped to the left one
894        assert_eq!(
895            (at(&l, [-0.25, 0.25], wrap), at(&l, [-0.25, 0.25], clamp)),
896            (1.0, 0.0)
897        );
898        let c = l.point(0, [0.25, 0.75], wrap, Transfer::SrgbColour).unwrap();
899        assert_eq!(c[0], crate::color::srgb8_to_linear(128));
900        assert!(l.point(0, [f64::NAN, 0.0], wrap, Transfer::Stored).is_err());
901        assert_eq!(
902            [-3.0, 0.49, 0.5, 1.2, 9.0].map(|lod| l.nearest_level(lod).unwrap()),
903            [0, 0, 1, 1, 1]
904        );
905        assert!(l.nearest_level(f64::NAN).is_err());
906        let up = l.clone().rows_reversed().unwrap();
907        assert_eq!(at(&up, [0.25, 0.25], wrap), 128.0 / 255.0);
908        assert_eq!(at(&up, [0.25, 0.75], wrap), 0.0);
909        assert_eq!(up.levels[1], vec![64; 4]);
910        let short = Levels {
911            width: 2,
912            height: 2,
913            levels: vec![vec![0; 12]],
914        };
915        assert!(short.rows_reversed().is_err());
916    }
917
918    /// Float texels as held: a 2 x 2 level point-sampled (the texel a coordinate falls in, addressed) and bilinear
919    /// (the middle is the mean of the four); a count other than the size's, or no texel, is refused; a 2D sample of a
920    /// volume is refused.
921    #[test]
922    fn float_texels_point_and_bilinear() {
923        let red_green = vec![
924            [0.0, 0.0, 0.0, 1.0],
925            [1.0, 0.0, 0.0, 1.0],
926            [0.0, 1.0, 0.0, 1.0],
927            [1.0, 1.0, 0.0, 1.0],
928        ];
929        let d = Decoded::from_texels(2, 2, red_green.clone()).unwrap();
930        let (wrap, clamp) = ([Address::Wrap; 2], [Address::Clamp; 2]);
931        assert_eq!(d.point([0.25, 0.25], clamp).unwrap(), [0.0, 0.0, 0.0, 1.0]);
932        assert_eq!(d.point([0.75, 0.25], clamp).unwrap(), [1.0, 0.0, 0.0, 1.0]);
933        assert_eq!(d.point([0.25, 0.75], clamp).unwrap(), [0.0, 1.0, 0.0, 1.0]);
934        // past the edges: clamped to the edge texel, wrapped round
935        assert_eq!(d.point([1.75, -3.0], clamp).unwrap(), [1.0, 0.0, 0.0, 1.0]);
936        assert_eq!(d.point([1.25, 0.25], wrap).unwrap(), [0.0, 0.0, 0.0, 1.0]);
937        assert_eq!(d.point([-0.25, 0.25], wrap).unwrap(), [1.0, 0.0, 0.0, 1.0]);
938        assert_eq!(d.bilinear([0.5, 0.5], clamp).unwrap(), [0.5, 0.5, 0.0, 1.0]);
939        assert_eq!(d.bilinear([0.375, 0.25], clamp).unwrap(), [0.25, 0.0, 0.0, 1.0]);
940        // values past 0..1 are filtered as they are
941        let hdr = Decoded::from_texels(2, 1, vec![[-2.0; 4], [6.0; 4]]).unwrap();
942        assert_eq!(hdr.bilinear([0.5, 0.5], clamp).unwrap(), [2.0; 4]);
943        assert!(Decoded::from_texels(2, 2, red_green[..3].to_vec()).is_err());
944        assert!(Decoded::from_texels(0, 2, Vec::new()).is_err());
945        assert!(d.point([f32::NAN, 0.5], clamp).is_err());
946        let volume = Decoded::volume(1, 1, 2, vec![[0.0; 4], [1.0; 4]]).unwrap();
947        assert!(volume.point([0.5, 0.5], clamp).is_err());
948        assert!(volume.bilinear([0.5, 0.5], clamp).is_err());
949    }
950
951    /// A 2 x 2 x 2 volume whose texel (x, y, z) is (x, y, z, 1): point sampling takes the texel a coordinate falls in;
952    /// linear filtering between the eight texels gives the coordinate's position between their centres, clamped at the
953    /// edges and wrapped round; the texel a load reads is the one at its index, none past the size.
954    #[test]
955    fn volumes_point_linear_and_texels() {
956        let texels: Vec<[f32; 4]> = (0..8)
957            .map(|i| [(i % 2) as f32, (i / 2 % 2) as f32, (i / 4) as f32, 1.0])
958            .collect();
959        let v = Decoded::volume(2, 2, 2, texels).unwrap();
960        assert_eq!(v.depth(), 2);
961        let (wrap, clamp) = ([Address::Wrap; 3], [Address::Clamp; 3]);
962        assert_eq!(
963            v.point_3d([0.75, 0.25, 0.75], clamp).unwrap(),
964            [1.0, 0.0, 1.0, 1.0]
965        );
966        assert_eq!(
967            v.point_3d([0.25, 1.25, -0.25], wrap).unwrap(),
968            [0.0, 0.0, 1.0, 1.0]
969        );
970        // halfway between the centres on every axis, a quarter of the way along x, at a corner's centre
971        let exact = Subtexel::Exact;
972        assert_eq!(v.linear_3d([0.5; 3], clamp, exact).unwrap(), [0.5, 0.5, 0.5, 1.0]);
973        assert_eq!(
974            v.linear_3d([0.375, 0.25, 0.25], clamp, exact).unwrap(),
975            [0.25, 0.0, 0.0, 1.0]
976        );
977        assert_eq!(
978            v.linear_3d([0.25, 0.25, 0.75], clamp, exact).unwrap(),
979            [0.0, 0.0, 1.0, 1.0]
980        );
981        // at the left edge: clamped it is the edge texel, wrapped it blends the far one in
982        assert_eq!(v.linear_3d([0.0, 0.25, 0.25], clamp, exact).unwrap()[0], 0.0);
983        assert_eq!(v.linear_3d([0.0, 0.25, 0.25], wrap, exact).unwrap()[0], 0.5);
984        assert!(v.linear_3d([0.5, f32::INFINITY, 0.5], clamp, exact).is_err());
985        assert_eq!(v.texel(1, 0, 1), Some([1.0, 0.0, 1.0, 1.0]));
986        assert_eq!(v.texel(2, 0, 0), None);
987        assert_eq!(v.texel(0, -1, 0), None);
988        assert_eq!(v.texel(0, 0, 2), None);
989        // a 2D level is one slice
990        let d = Decoded::from_texels(1, 1, vec![[3.0; 4]]).unwrap();
991        assert_eq!(d.point_3d([0.5; 3], clamp).unwrap(), [3.0; 4]);
992        assert_eq!(d.texel(0, 0, 0), Some([3.0; 4]));
993        assert_eq!(d.texel(0, 0, 1), None);
994    }
995
996    /// A coordinate that isn't finite, or whose texel is past an i32's range, is refused, never sampled (NaN texels)
997    /// and never an overflow: in both precisions.
998    #[test]
999    fn nonfinite_or_excessive_coordinates_are_refused() {
1000        let l = Levels::one(2, 2, vec![10; 16]).unwrap();
1001        let wrap = [Address::Wrap; 2];
1002        for uv in [[f64::NAN, 0.5], [0.5, f64::INFINITY], [1e300, 0.5], [0.5, -1e12]] {
1003            assert!(l.bilinear(0, uv, wrap, Transfer::Stored).is_err(), "{uv:?}");
1004            assert!(l.trilinear(uv, 0.0, wrap, Transfer::Stored).is_err(), "{uv:?}");
1005            assert!(
1006                l.decoded(0, Transfer::Stored)
1007                    .unwrap()
1008                    .bilinear(uv.map(|v| v as f32), wrap)
1009                    .is_err(),
1010                "{uv:?}"
1011            );
1012        }
1013        assert!(l.trilinear([1e8, -1e8], 0.0, wrap, Transfer::Stored).is_ok());
1014    }
1015}