use crate::color::srgb8_to_linear;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("invalid image: {0}")]
Invalid(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Address {
Wrap,
Clamp,
Mirror,
MirrorOnce,
Border,
}
impl Address {
fn index(self, i: i64, size: u32) -> Option<usize> {
let n = i64::from(size.max(1));
let (last, period) = (n.saturating_sub(1), n.saturating_mul(2));
let at = match self {
Address::Wrap => i.rem_euclid(n),
Address::Clamp => i.clamp(0, last),
Address::Mirror => {
let m = i.rem_euclid(period);
if m < n {
m
} else {
period.saturating_sub(1).saturating_sub(m)
}
}
Address::MirrorOnce => (if i < 0 { (-1i64).saturating_sub(i) } else { i }).clamp(0, last),
Address::Border if (0..n).contains(&i) => i,
Address::Border => return None,
};
Some(usize::try_from(at).unwrap_or_default())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Addressing<const N: usize> {
pub axes: [Address; N],
pub border: Option<[f32; 4]>,
}
impl<const N: usize> From<[Address; N]> for Addressing<N> {
fn from(axes: [Address; N]) -> Addressing<N> {
Addressing { axes, border: None }
}
}
impl<const N: usize> Addressing<N> {
pub fn bordered(axes: [Address; N], border: [f32; 4]) -> Addressing<N> {
Addressing {
axes,
border: Some(border),
}
}
fn border_colour(&self) -> Result<[f32; 4]> {
match self.border {
Some(c) if c.iter().any(|v| !v.is_finite()) => Err(Error::Invalid(format!(
"sampled texture: border colour {c:?} is not finite"
))),
Some(c) => Ok(c),
None if self.axes.contains(&Address::Border) => Err(Error::Invalid(
"sampled texture: border addressing with no border colour".into(),
)),
None => Ok([0.0; 4]),
}
}
fn indices(&self, at: [i64; N], size: [u32; N]) -> Option<[usize; N]> {
let mut out = [0usize; N];
for (o, ((a, i), n)) in out.iter_mut().zip(self.axes.iter().zip(at).zip(size)) {
*o = a.index(i, n)?;
}
Some(out)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Transfer {
Stored,
SrgbColour,
}
impl Transfer {
pub fn channel(self, c: usize, byte: u8) -> f64 {
match self {
Transfer::SrgbColour if c < 3 => srgb8_to_linear(byte),
_ => f64::from(byte) / 255.0,
}
}
}
#[derive(Clone, Debug)]
pub struct Levels {
pub width: u32,
pub height: u32,
pub levels: Vec<Vec<u8>>,
}
impl Levels {
pub fn one(width: u32, height: u32, rgba: Vec<u8>) -> Result<Levels> {
let count = (width as usize)
.checked_mul(height as usize)
.filter(|&n| n > 0)
.ok_or_else(|| Error::Invalid("sampled texture: invalid dimensions".into()))?;
if count.checked_mul(4) != Some(rgba.len()) {
return Err(Error::Invalid("sampled texture: RGBA length mismatch".into()));
}
Ok(Levels {
width,
height,
levels: vec![rgba],
})
}
pub fn rows_reversed(mut self) -> Result<Levels> {
for i in 0..self.levels.len() {
let row = self.level(i)?.row_bytes;
let Some(level) = self.levels.get_mut(i) else {
continue;
};
*level = level.chunks_exact(row).rev().flatten().copied().collect();
}
Ok(self)
}
pub fn size(&self, level: usize) -> (u32, u32) {
let s = u32::try_from(level).unwrap_or(31);
(
(self.width >> s.min(31)).max(1),
(self.height >> s.min(31)).max(1),
)
}
fn level(&self, level: usize) -> Result<Level<'_>> {
let (w, h) = self.size(level);
let bytes = self.levels.get(level).ok_or_else(|| {
Error::Invalid(format!(
"sampled texture: level {level} requested, expected one of the {} stored levels",
self.levels.len()
))
})?;
let row_bytes = (w as usize).checked_mul(4);
let want = row_bytes.and_then(|r| r.checked_mul(h as usize));
match (row_bytes, want) {
(Some(row_bytes), Some(want)) if want == bytes.len() && want > 0 => Ok(Level {
bytes,
row_bytes,
width: w,
height: h,
}),
_ => Err(Error::Invalid(format!(
"sampled texture: level {level} holds {} bytes, expected {h} rows of {w} RGBA8 texels",
bytes.len()
))),
}
}
pub fn texel(&self, level: usize, x: i64, y: i64, address: [Address; 2]) -> Result<[u8; 4]> {
self.level(level)?
.texel(x, y, &Addressing::from(address))?
.ok_or_else(|| {
Error::Invalid(format!(
"sampled texture: texel ({x}, {y}) is outside a border axis"
))
})
}
pub fn bilinear(
&self,
level: usize,
uv: [f64; 2],
address: impl Into<Addressing<2>>,
transfer: Transfer,
) -> Result<[f64; 4]> {
let address: Addressing<2> = address.into();
let border = address.border_colour()?;
let (w, h) = self.size(level);
let level = self.level(level)?;
let (x, y) = texel_coordinate([uv[0] * f64::from(w) - 0.5, uv[1] * f64::from(h) - 0.5])?;
let (fx, fy) = (x.floor(), y.floor());
let (tx, ty) = (x - fx, y - fy);
let (ix, iy) = (floor_f64(fx), floor_f64(fy));
let mut out = [0.0; 4];
for (dx, dy, wt) in [
(0, 0, (1.0 - tx) * (1.0 - ty)),
(1, 0, tx * (1.0 - ty)),
(0, 1, (1.0 - tx) * ty),
(1, 1, tx * ty),
] {
let t = level.texel(ix.saturating_add(dx), iy.saturating_add(dy), &address)?;
for (c, o) in out.iter_mut().enumerate() {
*o += wt * texel_channel(t, c, transfer, border);
}
}
Ok(out)
}
pub fn point(
&self,
level: usize,
uv: [f64; 2],
address: impl Into<Addressing<2>>,
transfer: Transfer,
) -> Result<[f64; 4]> {
let address: Addressing<2> = address.into();
let border = address.border_colour()?;
let (w, h) = self.size(level);
let level = self.level(level)?;
let (x, y) = texel_coordinate([uv[0] * f64::from(w), uv[1] * f64::from(h)])?;
let t = level.texel(floor_f64(x), floor_f64(y), &address)?;
Ok(std::array::from_fn(|c| texel_channel(t, c, transfer, border)))
}
pub fn decoded(&self, level: usize, transfer: Transfer) -> Result<Decoded> {
let l = self.level(level)?;
let texels = l
.bytes
.as_chunks::<4>()
.0
.iter()
.map(|p| {
let mut texel = [0f32; 4];
for (c, (out, &byte)) in texel.iter_mut().zip(p).enumerate() {
*out = match transfer {
Transfer::SrgbColour if c < 3 => to_f32(srgb8_to_linear(byte)),
_ => f32::from(byte) / 255.0,
};
}
texel
})
.collect();
Ok(Decoded {
width: l.width,
height: l.height,
depth: 1,
texels,
})
}
}
struct Level<'a> {
bytes: &'a [u8],
row_bytes: usize,
width: u32,
height: u32,
}
fn texel_channel(texel: Option<[u8; 4]>, c: usize, transfer: Transfer, border: [f32; 4]) -> f64 {
match texel {
Some(t) => transfer.channel(c, t.get(c).copied().unwrap_or_default()),
None => f64::from(border.get(c).copied().unwrap_or_default()),
}
}
impl Level<'_> {
fn texel(&self, x: i64, y: i64, address: &Addressing<2>) -> Result<Option<[u8; 4]>> {
let Some([x, y]) = address.indices([x, y], [self.width, self.height]) else {
return Ok(None);
};
let i = y
.checked_mul(self.row_bytes)
.and_then(|row| row.checked_add(x.checked_mul(4)?));
i.and_then(|i| self.bytes.get(i..)?.first_chunk::<4>())
.copied()
.map(Some)
.ok_or_else(|| {
Error::Invalid(format!(
"sampled texture: texel ({x}, {y}) outside the level's {} bytes",
self.bytes.len()
))
})
}
}
fn texel_coordinate([x, y]: [f64; 2]) -> Result<(f64, f64)> {
if !x.is_finite() || !y.is_finite() || x.abs() >= 2147483520.0 || y.abs() >= 2147483520.0 {
return Err(Error::Invalid(
"sampled texture: nonfinite or excessive texel coordinate".into(),
));
}
Ok((x, y))
}
pub fn footprint_lod(width: u32, height: u32, gradients: [[f64; 2]; 2], bias: f64) -> f64 {
let texels = |d: [f64; 2]| (d[0] * f64::from(width)).hypot(d[1] * f64::from(height));
texels(gradients[0]).max(texels(gradients[1])).log2() + bias
}
impl Levels {
pub fn trilinear(
&self,
uv: [f64; 2],
lod: f64,
address: impl Into<Addressing<2>>,
transfer: Transfer,
) -> Result<[f64; 4]> {
let address: Addressing<2> = address.into();
if lod.is_nan() {
return Err(Error::Invalid(
"sampled texture: a level of detail that isn't a number".into(),
));
}
let last = self.levels.len().saturating_sub(1);
let lod = lod.clamp(0.0, last as f64);
let below = lod.floor();
let (l0, f) = (level_number(below), lod - below);
let a = self.bilinear(l0, uv, address, transfer)?;
if f == 0.0 {
return Ok(a);
}
let b = self.bilinear(l0.saturating_add(1).min(last), uv, address, transfer)?;
let mut mixed = [0.0; 4];
for (m, (a, b)) in mixed.iter_mut().zip(a.into_iter().zip(b)) {
*m = a + (b - a) * f;
}
Ok(mixed)
}
pub fn nearest_level(&self, lod: f64) -> Result<usize> {
if lod.is_nan() {
return Err(Error::Invalid(
"sampled texture: a level of detail that isn't a number".into(),
));
}
let last = self.levels.len().saturating_sub(1);
Ok(level_number((lod + 0.5).floor().clamp(0.0, last as f64)))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Subtexel {
Exact,
D3d11,
}
impl Subtexel {
fn resolve(self, x: f32) -> f32 {
match self {
Subtexel::Exact => x,
Subtexel::D3d11 => (x * 256.).floor() / 256.,
}
}
}
#[derive(Clone, Debug)]
pub struct Decoded {
width: u32,
height: u32,
depth: u32,
texels: Vec<[f32; 4]>,
}
fn texel_coordinate_f32(x: f32) -> Result<f32> {
if !x.is_finite() || x.abs() >= 2147483520.0 {
return Err(Error::Invalid(
"sampled texture: nonfinite or excessive texel coordinate".into(),
));
}
Ok(x)
}
impl Decoded {
pub fn from_texels(width: u32, height: u32, texels: Vec<[f32; 4]>) -> Result<Decoded> {
Decoded::volume(width, height, 1, texels)
}
pub fn volume(width: u32, height: u32, depth: u32, texels: Vec<[f32; 4]>) -> Result<Decoded> {
let count = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(depth as usize))
.filter(|&n| n > 0)
.ok_or_else(|| Error::Invalid("sampled texture: invalid dimensions".into()))?;
if texels.len() != count {
return Err(Error::Invalid(format!(
"sampled texture: {} texels for {width} x {height} x {depth}",
texels.len()
)));
}
Ok(Decoded {
width,
height,
depth,
texels,
})
}
pub fn dimensions(&self) -> [usize; 2] {
[self.width as usize, self.height as usize]
}
pub fn depth(&self) -> usize {
self.depth as usize
}
pub fn decoded_bytes(&self) -> usize {
self.texels.len().saturating_mul(16)
}
fn get(&self, x: usize, y: usize, z: usize) -> Option<[f32; 4]> {
let index = z
.checked_mul(self.height as usize)?
.checked_add(y)?
.checked_mul(self.width as usize)?
.checked_add(x)?;
self.texels.get(index).copied()
}
pub fn texel(&self, x: i64, y: i64, z: i64) -> Option<[f32; 4]> {
let inside = |i: i64, n: u32| usize::try_from(i).ok().filter(|&i| i < n as usize);
self.get(
inside(x, self.width)?,
inside(y, self.height)?,
inside(z, self.depth)?,
)
}
fn at(&self, x: usize, y: usize, z: usize) -> Result<[f32; 4]> {
self.get(x, y, z).ok_or_else(|| {
Error::Invalid(format!(
"sampled texture: texel ({x}, {y}, {z}) outside the {} texels of a {} x {} x {} level",
self.texels.len(),
self.width,
self.height,
self.depth
))
})
}
fn plane(&self) -> Result<()> {
if self.depth == 1 {
Ok(())
} else {
Err(Error::Invalid(format!(
"sampled texture: a 2D sample of a volume of {} slices",
self.depth
)))
}
}
pub fn bilinear(&self, uv: [f32; 2], address: impl Into<Addressing<2>>) -> Result<[f32; 4]> {
self.bilinear_at(uv, address, Subtexel::Exact)
}
pub fn bilinear_at(
&self,
uv: [f32; 2],
address: impl Into<Addressing<2>>,
subtexel: Subtexel,
) -> Result<[f32; 4]> {
let address: Addressing<2> = address.into();
let border = address.border_colour()?;
self.plane()?;
let [u, v] = uv;
let x = texel_coordinate_f32(subtexel.resolve(u * self.width as f32 - 0.5))?;
let y = texel_coordinate_f32(subtexel.resolve(v * self.height as f32 - 0.5))?;
let (ix, iy) = (floor_f32(x), floor_f32(y));
let (tx, ty) = (x - ix as f32, y - iy as f32);
let mut result = [0.; 4];
for dy in 0..2 {
for dx in 0..2 {
let weight = (if dx == 1 { tx } else { 1. - tx }) * (if dy == 1 { ty } else { 1. - ty });
let texel = match address.indices(
[ix.saturating_add(dx), iy.saturating_add(dy)],
[self.width, self.height],
) {
Some([xx, yy]) => self.at(xx, yy, 0)?,
None => border,
};
for (out, t) in result.iter_mut().zip(texel) {
*out += t * weight;
}
}
}
Ok(result)
}
pub fn point(&self, uv: [f32; 2], address: impl Into<Addressing<2>>) -> Result<[f32; 4]> {
let address: Addressing<2> = address.into();
let border = address.border_colour()?;
self.plane()?;
let [u, v] = uv;
let x = texel_coordinate_f32(u * self.width as f32)?;
let y = texel_coordinate_f32(v * self.height as f32)?;
match address.indices([floor_f32(x), floor_f32(y)], [self.width, self.height]) {
Some([xx, yy]) => self.at(xx, yy, 0),
None => Ok(border),
}
}
pub fn point_3d(&self, uvw: [f32; 3], address: impl Into<Addressing<3>>) -> Result<[f32; 4]> {
let address: Addressing<3> = address.into();
let border = address.border_colour()?;
let [u, v, w] = uvw;
let axis = |c: f32, n: u32| -> Result<i64> { Ok(floor_f32(texel_coordinate_f32(c * n as f32)?)) };
let at = [axis(u, self.width)?, axis(v, self.height)?, axis(w, self.depth)?];
match address.indices(at, [self.width, self.height, self.depth]) {
Some([x, y, z]) => self.at(x, y, z),
None => Ok(border),
}
}
pub fn linear_3d(
&self,
uvw: [f32; 3],
address: impl Into<Addressing<3>>,
subtexel: Subtexel,
) -> Result<[f32; 4]> {
let address: Addressing<3> = address.into();
let border = address.border_colour()?;
let [u, v, w] = uvw;
let (sw, sh, sd) = (self.width, self.height, self.depth);
let axis = |c: f32, n: u32| -> Result<(i64, f32)> {
let t = texel_coordinate_f32(subtexel.resolve(c * n as f32 - 0.5))?;
let base = floor_f32(t);
Ok((base, t - base as f32))
};
let ((bx, tx), (by, ty), (bz, tz)) = (axis(u, sw)?, axis(v, sh)?, axis(w, sd)?);
let weight = |d: i64, t: f32| if d == 1 { t } else { 1. - t };
let mut result = [0.; 4];
for dz in 0..2 {
for dy in 0..2 {
for dx in 0..2 {
let texel = match address.indices(
[
bx.saturating_add(dx),
by.saturating_add(dy),
bz.saturating_add(dz),
],
[sw, sh, sd],
) {
Some([x, y, z]) => self.at(x, y, z)?,
None => border,
};
let wt = weight(dx, tx) * weight(dy, ty) * weight(dz, tz);
for (out, t) in result.iter_mut().zip(texel) {
*out += t * wt;
}
}
}
}
Ok(result)
}
}
#[allow(
clippy::cast_possible_truncation,
reason = "the caller checked the coordinate is finite and within an i32's range"
)]
fn floor_f32(x: f32) -> i64 {
x.floor() as i64
}
#[allow(
clippy::cast_possible_truncation,
reason = "the caller checked the coordinate is finite and within an i32's range"
)]
fn floor_f64(x: f64) -> i64 {
x.floor() as i64
}
#[allow(
clippy::cast_possible_truncation,
reason = "rounding the f64 curve value to the f32 texel is the documented decode"
)]
fn to_f32(x: f64) -> f32 {
x as f32
}
#[allow(
clippy::cast_possible_truncation,
reason = "the caller clamped the value to a stored level number"
)]
fn level_number(lod: f64) -> usize {
lod as usize
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "test fixtures")]
mod tests {
use super::*;
#[test]
fn addressing_out_of_range() {
let read = |a: Address, from: i64, to: i64| -> Vec<usize> {
(from..=to).map(|i| a.index(i, 4).unwrap()).collect()
};
assert_eq!(read(Address::Wrap, -5, 5), [3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1]);
assert_eq!(
read(Address::Mirror, -5, 8),
[3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 2, 1, 0, 0]
);
assert_eq!(read(Address::Clamp, -2, 5), [0, 0, 0, 1, 2, 3, 3, 3]);
assert_eq!(
read(Address::MirrorOnce, -5, 5),
[3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 3]
);
let border: Vec<Option<usize>> = (-2..=5).map(|i| Address::Border.index(i, 4)).collect();
assert_eq!(
border,
[None, None, Some(0), Some(1), Some(2), Some(3), None, None]
);
}
#[test]
fn border_addressing_reads_the_stated_colour() {
let l = Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255]).unwrap();
let axes = [Address::Border, Address::Clamp];
let (zero, one) = (
Addressing::bordered(axes, [0.0; 4]),
Addressing::bordered(axes, [1.0; 4]),
);
assert_eq!(
l.point(0, [-0.25, 0.5], zero, Transfer::Stored).unwrap(),
[0.0; 4]
);
assert_eq!(l.point(0, [-0.25, 0.5], one, Transfer::Stored).unwrap(), [1.0; 4]);
assert_eq!(
l.point(0, [0.25, 0.5], one, Transfer::Stored).unwrap(),
[0.0, 0.0, 0.0, 1.0]
);
assert_eq!(
l.bilinear(0, [0.0, 0.5], one, Transfer::Stored).unwrap(),
[0.5, 0.5, 0.5, 1.0]
);
assert_eq!(
l.bilinear(0, [0.0, 0.5], zero, Transfer::Stored).unwrap(),
[0.0, 0.0, 0.0, 0.5]
);
let grey = Addressing::bordered(axes, [0.5; 4]);
assert_eq!(
l.point(0, [-0.25, 0.5], grey, Transfer::SrgbColour).unwrap(),
[0.5; 4]
);
assert_eq!(
l.trilinear([-1.0, 0.5], 0.0, one, Transfer::Stored).unwrap(),
[1.0; 4]
);
let d = l.decoded(0, Transfer::Stored).unwrap();
assert_eq!(d.point([1.5, 0.5], one).unwrap(), [1.0; 4]);
assert_eq!(d.bilinear([0.0, 0.5], one).unwrap(), [0.5, 0.5, 0.5, 1.0]);
assert_eq!(d.bilinear([0.0, 0.5], zero).unwrap(), [0.0, 0.0, 0.0, 0.5]);
let v = Decoded::volume(1, 1, 2, vec![[0.25; 4], [0.75; 4]]).unwrap();
let volume = Addressing::bordered([Address::Clamp, Address::Clamp, Address::Border], [1.0; 4]);
assert_eq!(v.point_3d([0.5, 0.5, 1.5], volume).unwrap(), [1.0; 4]);
assert_eq!(v.point_3d([0.5, 0.5, 0.25], volume).unwrap(), [0.25; 4]);
assert_eq!(
v.linear_3d([0.5, 0.5, 1.0], volume, Subtexel::Exact).unwrap(),
[0.875; 4]
);
assert!(l.point(0, [0.25, 0.5], axes, Transfer::Stored).is_err());
assert!(d.bilinear([0.5, 0.5], axes).is_err());
let nan = Addressing::bordered(axes, [f32::NAN, 0.0, 0.0, 0.0]);
assert!(l.bilinear(0, [0.5, 0.5], nan, Transfer::Stored).is_err());
assert!(l.texel(0, -1, 0, axes).is_err());
assert_eq!(l.texel(0, 1, 0, axes).unwrap(), [255; 4]);
assert_eq!(
Addressing::from([Address::Wrap; 2]),
Addressing {
axes: [Address::Wrap; 2],
border: None
}
);
}
#[test]
fn d3d11_subtexel_precision_truncates_to_eight_bits() {
let d = Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255])
.unwrap()
.decoded(0, Transfer::Stored)
.unwrap();
let clamp = [Address::Clamp; 2];
let u = 0.4f32;
let exact = d.bilinear_at([u, 0.5], clamp, Subtexel::Exact).unwrap()[0];
let d3d = d.bilinear_at([u, 0.5], clamp, Subtexel::D3d11).unwrap()[0];
assert!((exact - 0.3).abs() < 1e-6, "{exact}");
assert_eq!(d3d, 76. / 256.);
let on_step = (64. / 256. + 0.5) / 2.;
assert_eq!(
d.bilinear_at([on_step, 0.5], clamp, Subtexel::D3d11).unwrap(),
d.bilinear_at([on_step, 0.5], clamp, Subtexel::Exact).unwrap()
);
}
#[test]
fn bilinear_between_texel_centres() {
let l = Levels::one(2, 1, vec![0, 0, 0, 0, 255, 255, 255, 255]).unwrap();
let wrap = [Address::Wrap; 2];
let clamp = [Address::Clamp; 2];
assert_eq!(
l.bilinear(0, [0.5, 0.5], wrap, Transfer::Stored).unwrap(),
[0.5; 4]
);
assert_eq!(
l.bilinear(0, [0.25, 0.5], wrap, Transfer::Stored).unwrap(),
[0.0; 4]
);
assert_eq!(
l.bilinear(0, [0.0, 0.5], wrap, Transfer::Stored).unwrap(),
[0.5; 4]
);
assert_eq!(
l.bilinear(0, [0.0, 0.5], clamp, Transfer::Stored).unwrap(),
[0.0; 4]
);
let d = l.decoded(0, Transfer::SrgbColour).unwrap();
let half = d.bilinear([0.5, 0.5], wrap).unwrap();
assert!(half.iter().all(|c| (c - 0.5).abs() < 1e-6), "{half:?}");
let g = Levels::one(1, 1, vec![128, 128, 128, 128]).unwrap();
let c = g.bilinear(0, [0.5, 0.5], wrap, Transfer::SrgbColour).unwrap();
assert!(
(c[0] - crate::color::srgb_to_linear(128.0 / 255.0)).abs() < 1e-15
&& (c[3] - 128.0 / 255.0).abs() < 1e-15
);
}
#[test]
fn trilinear_at_a_footprint() {
assert_eq!(
footprint_lod(8, 4, [[1.0 / 8.0, 0.0], [0.0, 1.0 / 4.0]], 0.0),
0.0
);
assert_eq!(footprint_lod(8, 4, [[0.0, 0.0], [0.0, 1.0]], 0.0), 2.0);
assert_eq!(footprint_lod(8, 4, [[0.0; 2]; 2], 3.0), f64::NEG_INFINITY);
let l = Levels {
width: 2,
height: 2,
levels: vec![vec![0; 16], vec![255; 4]],
};
let wrap = [Address::Wrap; 2];
let at = |lod| l.trilinear([0.5, 0.5], lod, wrap, Transfer::Stored).unwrap()[0];
assert_eq!((at(-1.0), at(0.25), at(1.0), at(7.0)), (0.0, 0.25, 1.0, 1.0));
assert!(l.trilinear([0.5, 0.5], f64::NAN, wrap, Transfer::Stored).is_err());
}
#[test]
fn point_filtering_nearest_levels_and_reversed_rows() {
let l = Levels {
width: 2,
height: 2,
levels: vec![[[0; 4], [255; 4], [128; 4], [255; 4]].concat(), vec![64; 4]],
};
let (wrap, clamp) = ([Address::Wrap; 2], [Address::Clamp; 2]);
let at = |l: &Levels, uv: [f64; 2], a| l.point(0, uv, a, Transfer::Stored).unwrap()[0];
assert_eq!(at(&l, [0.25, 0.25], wrap), 0.0);
assert_eq!(at(&l, [0.5, 0.25], wrap), 1.0);
assert_eq!(at(&l, [0.25, 0.75], wrap), 128.0 / 255.0);
assert_eq!(
(at(&l, [-0.25, 0.25], wrap), at(&l, [-0.25, 0.25], clamp)),
(1.0, 0.0)
);
let c = l.point(0, [0.25, 0.75], wrap, Transfer::SrgbColour).unwrap();
assert_eq!(c[0], crate::color::srgb8_to_linear(128));
assert!(l.point(0, [f64::NAN, 0.0], wrap, Transfer::Stored).is_err());
assert_eq!(
[-3.0, 0.49, 0.5, 1.2, 9.0].map(|lod| l.nearest_level(lod).unwrap()),
[0, 0, 1, 1, 1]
);
assert!(l.nearest_level(f64::NAN).is_err());
let up = l.clone().rows_reversed().unwrap();
assert_eq!(at(&up, [0.25, 0.25], wrap), 128.0 / 255.0);
assert_eq!(at(&up, [0.25, 0.75], wrap), 0.0);
assert_eq!(up.levels[1], vec![64; 4]);
let short = Levels {
width: 2,
height: 2,
levels: vec![vec![0; 12]],
};
assert!(short.rows_reversed().is_err());
}
#[test]
fn float_texels_point_and_bilinear() {
let red_green = vec![
[0.0, 0.0, 0.0, 1.0],
[1.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
];
let d = Decoded::from_texels(2, 2, red_green.clone()).unwrap();
let (wrap, clamp) = ([Address::Wrap; 2], [Address::Clamp; 2]);
assert_eq!(d.point([0.25, 0.25], clamp).unwrap(), [0.0, 0.0, 0.0, 1.0]);
assert_eq!(d.point([0.75, 0.25], clamp).unwrap(), [1.0, 0.0, 0.0, 1.0]);
assert_eq!(d.point([0.25, 0.75], clamp).unwrap(), [0.0, 1.0, 0.0, 1.0]);
assert_eq!(d.point([1.75, -3.0], clamp).unwrap(), [1.0, 0.0, 0.0, 1.0]);
assert_eq!(d.point([1.25, 0.25], wrap).unwrap(), [0.0, 0.0, 0.0, 1.0]);
assert_eq!(d.point([-0.25, 0.25], wrap).unwrap(), [1.0, 0.0, 0.0, 1.0]);
assert_eq!(d.bilinear([0.5, 0.5], clamp).unwrap(), [0.5, 0.5, 0.0, 1.0]);
assert_eq!(d.bilinear([0.375, 0.25], clamp).unwrap(), [0.25, 0.0, 0.0, 1.0]);
let hdr = Decoded::from_texels(2, 1, vec![[-2.0; 4], [6.0; 4]]).unwrap();
assert_eq!(hdr.bilinear([0.5, 0.5], clamp).unwrap(), [2.0; 4]);
assert!(Decoded::from_texels(2, 2, red_green[..3].to_vec()).is_err());
assert!(Decoded::from_texels(0, 2, Vec::new()).is_err());
assert!(d.point([f32::NAN, 0.5], clamp).is_err());
let volume = Decoded::volume(1, 1, 2, vec![[0.0; 4], [1.0; 4]]).unwrap();
assert!(volume.point([0.5, 0.5], clamp).is_err());
assert!(volume.bilinear([0.5, 0.5], clamp).is_err());
}
#[test]
fn volumes_point_linear_and_texels() {
let texels: Vec<[f32; 4]> = (0..8)
.map(|i| [(i % 2) as f32, (i / 2 % 2) as f32, (i / 4) as f32, 1.0])
.collect();
let v = Decoded::volume(2, 2, 2, texels).unwrap();
assert_eq!(v.depth(), 2);
let (wrap, clamp) = ([Address::Wrap; 3], [Address::Clamp; 3]);
assert_eq!(
v.point_3d([0.75, 0.25, 0.75], clamp).unwrap(),
[1.0, 0.0, 1.0, 1.0]
);
assert_eq!(
v.point_3d([0.25, 1.25, -0.25], wrap).unwrap(),
[0.0, 0.0, 1.0, 1.0]
);
let exact = Subtexel::Exact;
assert_eq!(v.linear_3d([0.5; 3], clamp, exact).unwrap(), [0.5, 0.5, 0.5, 1.0]);
assert_eq!(
v.linear_3d([0.375, 0.25, 0.25], clamp, exact).unwrap(),
[0.25, 0.0, 0.0, 1.0]
);
assert_eq!(
v.linear_3d([0.25, 0.25, 0.75], clamp, exact).unwrap(),
[0.0, 0.0, 1.0, 1.0]
);
assert_eq!(v.linear_3d([0.0, 0.25, 0.25], clamp, exact).unwrap()[0], 0.0);
assert_eq!(v.linear_3d([0.0, 0.25, 0.25], wrap, exact).unwrap()[0], 0.5);
assert!(v.linear_3d([0.5, f32::INFINITY, 0.5], clamp, exact).is_err());
assert_eq!(v.texel(1, 0, 1), Some([1.0, 0.0, 1.0, 1.0]));
assert_eq!(v.texel(2, 0, 0), None);
assert_eq!(v.texel(0, -1, 0), None);
assert_eq!(v.texel(0, 0, 2), None);
let d = Decoded::from_texels(1, 1, vec![[3.0; 4]]).unwrap();
assert_eq!(d.point_3d([0.5; 3], clamp).unwrap(), [3.0; 4]);
assert_eq!(d.texel(0, 0, 0), Some([3.0; 4]));
assert_eq!(d.texel(0, 0, 1), None);
}
#[test]
fn nonfinite_or_excessive_coordinates_are_refused() {
let l = Levels::one(2, 2, vec![10; 16]).unwrap();
let wrap = [Address::Wrap; 2];
for uv in [[f64::NAN, 0.5], [0.5, f64::INFINITY], [1e300, 0.5], [0.5, -1e12]] {
assert!(l.bilinear(0, uv, wrap, Transfer::Stored).is_err(), "{uv:?}");
assert!(l.trilinear(uv, 0.0, wrap, Transfer::Stored).is_err(), "{uv:?}");
assert!(
l.decoded(0, Transfer::Stored)
.unwrap()
.bilinear(uv.map(|v| v as f32), wrap)
.is_err(),
"{uv:?}"
);
}
assert!(l.trilinear([1e8, -1e8], 0.0, wrap, Transfer::Stored).is_ok());
}
}