Skip to main content

iridia_exec/
texture.rs

1//! A 2D texture as the executor samples it ([`SampledTexture`], an [`Image`]): its stored levels, the colour transfer
2//! applied before filtering, the addressing (with a border's colour), level-of-detail bias and level range its binding's
3//! sampler states ([`SamplerUse`]), and the filtering a caller states ([`Filtering`]).
4//!
5//! The filtering is the caller's policy: what a source states of its sampler's filter may be absent or not modelled, so
6//! a caller says how it filters (the base level bilinear, point or bilinear at the nearest level, or trilinear) and
7//! reports it. The addressing, the bias and the level range are the binding's data: an axis whose addressing the sampler
8//! state leaves unstated is the caller's stated assumption ([`Unstated`]), never a silent wrap; a border reads the colour
9//! the state gives it, and a border with none is refused; a stated bias and level range are applied where the level of
10//! detail is computed, or named as not applied ([`SampledTexture::unhonoured`]).
11//!
12//! The level of detail is the quad footprint's, or the level the program states. A sample at an implicit level outside
13//! a quad (an evaluation one pixel at a time) has no footprint: it is refused, unless the caller states the level such
14//! a sample takes ([`SampledTexture::with_footprintless_level`]).
15
16use iridia_interface::{AddressMode, SamplerState};
17use primase_foundations::{
18    sampling::Address, sampling::Addressing, sampling::Decoded, sampling::Levels, sampling::Subtexel,
19    sampling::Transfer, sampling::footprint_lod,
20};
21
22use crate::{Image, Lod};
23
24/// How a texture is filtered: the caller's stated policy.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Filtering {
27    /// The base level only, whatever level of detail the program asks for: bilinear in f32 over texels decoded once,
28    /// the texel coordinate resolved to `subtexel` precision ([`Decoded::bilinear_at`]).
29    BaseBilinear {
30        /// The precision the texel coordinate is resolved to.
31        subtexel: Subtexel,
32    },
33    /// The texel the coordinate falls in, of the stored level nearest the level of detail ([`Levels::point`],
34    /// [`Levels::nearest_level`]), in f64: a point filter with a point mip filter.
35    Point,
36    /// Bilinear in f64 ([`Levels::bilinear`]) in the stored level nearest the level of detail: a linear filter with a
37    /// point mip filter.
38    Bilinear,
39    /// The two stored levels about the level of detail mixed, each bilinear in f64 ([`Levels::trilinear`]).
40    Trilinear,
41}
42
43impl Filtering {
44    /// The level of detail [`Filtering::Point`], [`Filtering::Bilinear`] and [`Filtering::Trilinear`] sample at: the
45    /// quad footprint's ([`footprint_lod`]: the program's bias, clamped to Direct3D's -16..15.99, then the sampler's
46    /// bias, added to the footprint's base-2 logarithm), or the level the program states plus the sampler's bias
47    /// (Direct3D's `sample_l` honours the sampler's MipLODBias). Without a footprint, an implicit level is
48    /// `footprintless` (plus the program's clamped bias, then the sampler's), refused when the caller states none.
49    fn level_of_detail(
50        lod: Lod,
51        size: [u32; 2],
52        sampler_bias: f64,
53        footprintless: Option<f32>,
54    ) -> Result<f64, String> {
55        let [w, h] = size;
56        let grad = |g: [[f32; 2]; 2]| g.map(|d| d.map(f64::from));
57        let program = |b: f32| f64::from(b.clamp(-16.0, 15.99));
58        let without = || {
59            footprintless
60                .map(f64::from)
61                .ok_or_else(|| "an implicit level without a quad's footprint".to_owned())
62        };
63        Ok(match lod {
64            Lod::Gradients(g) => footprint_lod(w, h, grad(g), sampler_bias),
65            Lod::GradientsBias(g, b) => footprint_lod(w, h, grad(g), program(b) + sampler_bias),
66            Lod::Level(l) => f64::from(l) + sampler_bias,
67            Lod::Implicit => without()? + sampler_bias,
68            Lod::Bias(b) => without()? + program(b) + sampler_bias,
69        })
70    }
71}
72
73/// What an addressing axis the sampler state leaves unstated is.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum Unstated {
76    /// Refused: the binding must state it.
77    Refuse,
78    /// The caller's stated assumption, which it reports.
79    Assume(Address),
80}
81
82/// What a binding's sampler gives a texture's sampling: the addressing along u and v, the colour a border reads, and
83/// the level-of-detail bias and level range it states.
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub struct SamplerUse {
86    /// The addressing along u and v.
87    pub address: [Address; 2],
88    /// The colour a coordinate outside the texture reads along an [`Address::Border`] axis; finite, and stated where an
89    /// axis is a border.
90    pub border: Option<[f32; 4]>,
91    /// Added to a computed level of detail ([`Filtering::Trilinear`]); finite.
92    pub mip_lod_bias: Option<f32>,
93    /// The least and greatest level of detail the sampler reads: the level, its bias added, is clamped to them; finite
94    /// and ordered.
95    pub lod: Option<[f32; 2]>,
96}
97
98impl SamplerUse {
99    /// The addressing `address`, no border colour, no bias and no level range.
100    pub fn addressed(address: [Address; 2]) -> SamplerUse {
101        SamplerUse {
102            address,
103            border: None,
104            mip_lod_bias: None,
105            lod: None,
106        }
107    }
108
109    /// From a binding's sampler state: its u and v addressing (an unstated axis as `unstated` says), its border colour,
110    /// its bias and its level range. Border addressing with no border colour is refused, as is a state that does not
111    /// verify ([`SamplerState::verify`]: a colour, bias or range that isn't finite, a reversed range). Whether an axis
112    /// was assumed is [`SamplerUse::assumed`].
113    pub fn of(state: &SamplerState, unstated: Unstated) -> Result<SamplerUse, String> {
114        state.verify()?;
115        let axis = |k: usize| -> Result<Address, String> {
116            let name = ["u", "v"].get(k).copied().unwrap_or("w");
117            Ok(match state.wrap.get(k).copied().flatten() {
118                Some(AddressMode::Repeat) => Address::Wrap,
119                Some(AddressMode::Mirror) => Address::Mirror,
120                Some(AddressMode::Clamp) => Address::Clamp,
121                Some(AddressMode::MirrorOnce) => Address::MirrorOnce,
122                Some(AddressMode::Border) if state.border.is_some() => Address::Border,
123                Some(AddressMode::Border) => {
124                    return Err(format!(
125                        "border addressing along {name}: the sampler states no border colour"
126                    ));
127                }
128                None => match unstated {
129                    Unstated::Assume(a) => a,
130                    Unstated::Refuse => {
131                        return Err(format!("the sampler states no addressing along {name}"));
132                    }
133                },
134            })
135        };
136        Ok(SamplerUse {
137            address: [axis(0)?, axis(1)?],
138            border: state.border,
139            mip_lod_bias: state.mip_lod_bias,
140            lod: state.lod,
141        })
142    }
143
144    /// The addressing a filter takes: the axes and the border colour.
145    fn addressing(&self) -> Addressing<2> {
146        Addressing {
147            axes: self.address,
148            border: self.border,
149        }
150    }
151
152    /// `level` clamped to the stated level range, if any.
153    fn clamped(&self, level: f64) -> f64 {
154        match self.lod {
155            Some([low, high]) => level.clamp(f64::from(low), f64::from(high)),
156            None => level,
157        }
158    }
159
160    /// Whether `state` leaves the u or v addressing unstated: what [`SamplerUse::of`] took from [`Unstated`].
161    pub fn assumed(state: &SamplerState) -> bool {
162        state.wrap[0].is_none() || state.wrap[1].is_none()
163    }
164}
165
166/// A texture the executor samples (see the module documentation).
167#[derive(Clone, Debug)]
168pub struct SampledTexture {
169    levels: Levels,
170    transfer: Transfer,
171    sampler: SamplerUse,
172    filtering: Filtering,
173    /// The level an implicit-level sample without a footprint takes, when the caller states one.
174    footprintless: Option<f32>,
175    /// The base level decoded once, for [`Filtering::BaseBilinear`].
176    base: Option<Decoded>,
177}
178
179impl SampledTexture {
180    /// `levels` (at least one), read through `transfer`, sampled as `sampler` and `filtering` say; a sampler bias,
181    /// border colour or level range that isn't finite, a reversed range, and a border axis with no colour are refused.
182    pub fn new(
183        levels: Levels,
184        transfer: Transfer,
185        sampler: SamplerUse,
186        filtering: Filtering,
187    ) -> Result<SampledTexture, String> {
188        if levels.levels.is_empty() {
189            return Err("a texture with no stored level".into());
190        }
191        if let Some(b) = sampler.mip_lod_bias
192            && !b.is_finite()
193        {
194            return Err(format!("sampler bias {b} is not finite"));
195        }
196        if let Some([low, high]) = sampler.lod
197            && (!low.is_finite() || !high.is_finite() || low > high)
198        {
199            return Err(format!(
200                "sampler level range {low}..{high} is not a finite, ordered range"
201            ));
202        }
203        match sampler.border {
204            Some(c) if c.iter().any(|v| !v.is_finite()) => {
205                return Err(format!("border colour {c:?} is not finite"));
206            }
207            None if sampler.address.contains(&Address::Border) => {
208                return Err("border addressing with no border colour".into());
209            }
210            _ => {}
211        }
212        let base = match filtering {
213            Filtering::BaseBilinear { .. } => {
214                Some(levels.decoded(0, transfer).map_err(|e| format!("level 0: {e}"))?)
215            }
216            Filtering::Point | Filtering::Bilinear | Filtering::Trilinear => None,
217        };
218        Ok(SampledTexture {
219            levels,
220            transfer,
221            sampler,
222            filtering,
223            footprintless: None,
224            base,
225        })
226    }
227
228    /// The same texture with `level` the level of detail of an implicit-level sample that has no quad footprint (an
229    /// evaluation one pixel at a time): the caller's stated assumption, which it reports. A level that isn't finite
230    /// is refused.
231    pub fn with_footprintless_level(mut self, level: f32) -> Result<SampledTexture, String> {
232        if !level.is_finite() {
233            return Err(format!("footprintless level {level} is not finite"));
234        }
235        self.footprintless = Some(level);
236        Ok(self)
237    }
238
239    /// The level an implicit-level sample without a footprint takes, if the caller stated one.
240    pub fn footprintless_level(&self) -> Option<f32> {
241        self.footprintless
242    }
243
244    /// The stored levels.
245    pub fn levels(&self) -> &Levels {
246        &self.levels
247    }
248
249    /// The transfer function the texels are decoded with.
250    pub fn transfer(&self) -> Transfer {
251        self.transfer
252    }
253
254    /// The sampler state the texture is sampled with.
255    pub fn sampler(&self) -> SamplerUse {
256        self.sampler
257    }
258
259    /// The addressing along u and v.
260    pub fn address(&self) -> [Address; 2] {
261        self.sampler.address
262    }
263
264    /// The filtering policy.
265    pub fn filtering(&self) -> Filtering {
266        self.filtering
267    }
268
269    /// The base level's width and height.
270    pub fn dimensions(&self) -> [usize; 2] {
271        [self.levels.width as usize, self.levels.height as usize]
272    }
273
274    /// The bytes it holds: the stored levels and the texels decoded for filtering.
275    pub fn decoded_bytes(&self) -> usize {
276        self.levels
277            .levels
278            .iter()
279            .map(Vec::len)
280            .fold(0usize, usize::saturating_add)
281            .saturating_add(self.base.as_ref().map_or(0, Decoded::decoded_bytes))
282    }
283
284    /// What of its sampler this sampling does not apply, for the caller to report: under [`Filtering::BaseBilinear`],
285    /// which reads the base level whatever the level of detail, a stated bias, and a stated level range that excludes
286    /// the base level.
287    pub fn unhonoured(&self) -> Option<&'static str> {
288        if !matches!(self.filtering, Filtering::BaseBilinear { .. }) {
289            return None;
290        }
291        let bias = self.sampler.mip_lod_bias.is_some_and(|b| b != 0.0);
292        let range = self.sampler.lod.is_some_and(|[low, _]| low > 0.0);
293        match (bias, range) {
294            (true, true) => Some("stated sampler bias and level range not applied (base level)"),
295            (true, false) => Some("stated sampler bias not applied (base level)"),
296            (false, true) => Some("stated sampler level range not applied (base level)"),
297            (false, false) => None,
298        }
299    }
300}
301
302impl Image for SampledTexture {
303    fn sample(&self, uv: [f32; 2], lod: Lod) -> Result<[f32; 4], String> {
304        let address = self.sampler.addressing();
305        let level = || {
306            Filtering::level_of_detail(
307                lod,
308                [self.levels.width, self.levels.height],
309                self.sampler.mip_lod_bias.map_or(0.0, f64::from),
310                self.footprintless,
311            )
312            .map(|l| self.sampler.clamped(l))
313        };
314        let (at, levels, transfer) = ([f64::from(uv[0]), f64::from(uv[1])], &self.levels, self.transfer);
315        let v = match (self.filtering, &self.base) {
316            (Filtering::BaseBilinear { subtexel }, Some(base)) => {
317                return base.bilinear_at(uv, address, subtexel).map_err(|e| e.to_string());
318            }
319            (Filtering::BaseBilinear { .. }, None) => {
320                return Err("a base-level texture whose base was not decoded when it was made".into());
321            }
322            (Filtering::Point, _) => levels
323                .nearest_level(level()?)
324                .and_then(|l| levels.point(l, at, address, transfer)),
325            (Filtering::Bilinear, _) => levels
326                .nearest_level(level()?)
327                .and_then(|l| levels.bilinear(l, at, address, transfer)),
328            (Filtering::Trilinear, _) => levels.trilinear(at, level()?, address, transfer),
329        }
330        .map_err(|e| e.to_string())?;
331        Ok(v.map(crate::narrow))
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    fn state(u: Option<AddressMode>, v: Option<AddressMode>, bias: Option<f32>) -> SamplerState {
340        SamplerState {
341            wrap: [u, v, None],
342            mip_lod_bias: bias,
343            ..SamplerState::default()
344        }
345    }
346
347    /// A 2 x 1 texture, black then white, one level.
348    fn two_texels() -> Levels {
349        Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255]).unwrap()
350    }
351
352    /// The stated addressing is what samples past the edge read; an unstated axis is the caller's assumption or a
353    /// refusal; border addressing and a non-finite bias are refused.
354    #[test]
355    fn the_sampler_states_the_addressing() {
356        let clamp = SamplerUse::of(
357            &state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), None),
358            Unstated::Refuse,
359        )
360        .unwrap();
361        assert_eq!(clamp.address, [Address::Clamp; 2]);
362        let base = Filtering::BaseBilinear {
363            subtexel: Subtexel::D3d11,
364        };
365        let clamped = SampledTexture::new(two_texels(), Transfer::Stored, clamp, base).unwrap();
366        let wrapped = SampledTexture::new(
367            two_texels(),
368            Transfer::Stored,
369            SamplerUse::addressed([Address::Wrap; 2]),
370            base,
371        )
372        .unwrap();
373        // u = 0: half a texel left of the first centre; clamped reads black, wrapped mixes in the white texel
374        assert_eq!(clamped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.0);
375        assert_eq!(wrapped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.5);
376        assert!(SamplerUse::of(&state(None, Some(AddressMode::Clamp), None), Unstated::Refuse).is_err());
377        let assumed = SamplerUse::of(
378            &state(None, Some(AddressMode::Clamp), None),
379            Unstated::Assume(Address::Wrap),
380        )
381        .unwrap();
382        assert_eq!(assumed.address, [Address::Wrap, Address::Clamp]);
383        assert!(SamplerUse::assumed(&state(None, Some(AddressMode::Clamp), None)));
384        // a border with no stated colour is refused
385        assert!(
386            SamplerUse::of(
387                &state(Some(AddressMode::Border), Some(AddressMode::Clamp), None),
388                Unstated::Refuse
389            )
390            .is_err()
391        );
392        assert!(
393            SamplerUse::of(
394                &state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), Some(f32::NAN)),
395                Unstated::Refuse
396            )
397            .is_err()
398        );
399    }
400
401    /// A stated border colour is what samples past a border axis read: black then white, sampled past the left edge
402    /// with borders 0 and 1; bilinear at the edge mixes half the border with half the black texel. A colour that isn't
403    /// finite is refused, and so is a border axis with no colour.
404    #[test]
405    fn a_stated_border_colour_is_read_past_the_edge() {
406        let bordered = |colour: [f32; 4]| {
407            let s = SamplerState {
408                border: Some(colour),
409                ..state(Some(AddressMode::Border), Some(AddressMode::Clamp), None)
410            };
411            let sampler = SamplerUse::of(&s, Unstated::Refuse).unwrap();
412            assert_eq!(sampler.address, [Address::Border, Address::Clamp]);
413            sampler
414        };
415        for filtering in [
416            Filtering::BaseBilinear {
417                subtexel: Subtexel::D3d11,
418            },
419            Filtering::Bilinear,
420        ] {
421            let zero =
422                SampledTexture::new(two_texels(), Transfer::Stored, bordered([0.0; 4]), filtering).unwrap();
423            let one =
424                SampledTexture::new(two_texels(), Transfer::Stored, bordered([1.0; 4]), filtering).unwrap();
425            // a quarter texel past the left edge: wholly the border
426            let past = |t: &SampledTexture| t.sample([-0.25, 0.5], Lod::Level(0.0)).unwrap();
427            assert_eq!(past(&zero), [0.0; 4]);
428            assert_eq!(past(&one), [1.0; 4]);
429            // the left edge: half the border, half the black texel (alpha 1)
430            let edge = |t: &SampledTexture| t.sample([0.0, 0.5], Lod::Level(0.0)).unwrap();
431            assert_eq!(edge(&zero), [0.0, 0.0, 0.0, 0.5]);
432            assert_eq!(edge(&one), [0.5, 0.5, 0.5, 1.0]);
433        }
434        let nan = SamplerState {
435            border: Some([f32::NAN; 4]),
436            ..state(Some(AddressMode::Border), Some(AddressMode::Clamp), None)
437        };
438        assert!(SamplerUse::of(&nan, Unstated::Refuse).is_err());
439        let mut colourless = SamplerUse::addressed([Address::Border; 2]);
440        assert!(
441            SampledTexture::new(two_texels(), Transfer::Stored, colourless, Filtering::Bilinear).is_err()
442        );
443        colourless.border = Some([0.0; 4]);
444        assert!(SampledTexture::new(two_texels(), Transfer::Stored, colourless, Filtering::Bilinear).is_ok());
445    }
446
447    /// A stated level range clamps the level of detail after the bias: levels 0 (black) and 1 (white), a range of
448    /// 0..0 reads level 0 wherever the level of detail is, a range of 1..1 reads level 1; the base level names a range
449    /// that excludes it as not applied. A reversed range is refused.
450    #[test]
451    fn a_stated_level_range_clamps_the_level() {
452        let levels = Levels {
453            width: 2,
454            height: 2,
455            levels: vec![[0, 0, 0, 255].repeat(4), vec![255; 4]],
456        };
457        let make = |lod: Option<[f32; 2]>, filtering| {
458            SampledTexture::new(
459                levels.clone(),
460                Transfer::Stored,
461                SamplerUse {
462                    lod,
463                    ..SamplerUse::addressed([Address::Wrap; 2])
464                },
465                filtering,
466            )
467            .unwrap()
468        };
469        let base_only = make(Some([0.0, 0.0]), Filtering::Trilinear);
470        assert_eq!(base_only.sample([0.5, 0.5], Lod::Level(1.0)).unwrap()[0], 0.0);
471        let first = make(Some([1.0, 1.0]), Filtering::Point);
472        assert_eq!(first.sample([0.5, 0.5], Lod::Level(0.0)).unwrap()[0], 1.0);
473        let unclamped = make(None, Filtering::Point);
474        assert_eq!(unclamped.sample([0.5, 0.5], Lod::Level(0.0)).unwrap()[0], 0.0);
475        let base = Filtering::BaseBilinear {
476            subtexel: Subtexel::D3d11,
477        };
478        assert_eq!(
479            make(Some([1.0, 1.0]), base).unhonoured(),
480            Some("stated sampler level range not applied (base level)")
481        );
482        assert_eq!(make(Some([0.0, 0.0]), base).unhonoured(), None);
483        let reversed = SamplerUse {
484            lod: Some([2.0, 1.0]),
485            ..SamplerUse::addressed([Address::Wrap; 2])
486        };
487        assert!(SampledTexture::new(levels.clone(), Transfer::Stored, reversed, Filtering::Point).is_err());
488    }
489
490    /// A stated bias moves the footprint's level and a stated level under trilinear filtering; the base level names
491    /// it as not applied; an implicit level is refused.
492    #[test]
493    fn a_stated_bias_is_applied_or_named() {
494        // two levels: 2 x 2 black, 1 x 1 white
495        let levels = Levels {
496            width: 2,
497            height: 2,
498            levels: vec![[0, 0, 0, 255].repeat(4), vec![255; 4]],
499        };
500        let make = |bias: Option<f32>, filtering| {
501            SampledTexture::new(
502                levels.clone(),
503                Transfer::Stored,
504                SamplerUse {
505                    mip_lod_bias: bias,
506                    ..SamplerUse::addressed([Address::Wrap; 2])
507                },
508                filtering,
509            )
510            .unwrap()
511        };
512        // a footprint of half a texel: level -1, clamped to 0 (black); a bias of 1 more reaches level 0 still, 2 more
513        // reaches level 1 (white)
514        let footprint = Lod::Gradients([[0.25, 0.0], [0.0, 0.25]]);
515        let plain = make(None, Filtering::Trilinear);
516        assert_eq!(plain.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
517        let biased = make(Some(2.0), Filtering::Trilinear);
518        assert_eq!(biased.sample([0.5, 0.5], footprint).unwrap()[0], 1.0);
519        assert_eq!(biased.unhonoured(), None);
520        // a stated level takes the sampler's bias too: level -1 biased by 2 is level 1 (white); unbiased, level 0
521        assert_eq!(biased.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 1.0);
522        assert_eq!(plain.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 0.0);
523        assert_eq!(plain.sample([0.5, 0.5], Lod::Level(1.0)).unwrap()[0], 1.0);
524        assert!(plain.sample([0.5, 0.5], Lod::Implicit).is_err());
525        let base = make(
526            Some(2.0),
527            Filtering::BaseBilinear {
528                subtexel: Subtexel::D3d11,
529            },
530        );
531        assert_eq!(base.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
532        assert_eq!(
533            base.unhonoured(),
534            Some("stated sampler bias not applied (base level)")
535        );
536    }
537
538    /// Point and bilinear filtering read the stored level nearest the level of detail (a half rounds up); an implicit
539    /// level without a footprint is refused unless the caller states the level it takes, and a program's bias then
540    /// adds to that level.
541    #[test]
542    fn nearest_level_filters_and_a_stated_footprintless_level() {
543        // two levels: 2 x 2 with columns black and white, 1 x 1 grey
544        let levels = Levels {
545            width: 2,
546            height: 2,
547            levels: vec![[[0, 0, 0, 255], [255; 4]].concat().repeat(2), vec![128; 4]],
548        };
549        let make = |filtering| {
550            SampledTexture::new(
551                levels.clone(),
552                Transfer::Stored,
553                SamplerUse::addressed([Address::Wrap; 2]),
554                filtering,
555            )
556            .unwrap()
557        };
558        let grey = 128.0 / 255.0;
559        let point = make(Filtering::Point);
560        assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.0)).unwrap()[0], 0.0);
561        assert_eq!(point.sample([0.75, 0.25], Lod::Level(0.49)).unwrap()[0], 1.0);
562        assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.5)).unwrap()[0], grey);
563        let bilinear = make(Filtering::Bilinear);
564        assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.4)).unwrap()[0], 0.5);
565        assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.6)).unwrap()[0], grey);
566        assert!(point.sample([0.25, 0.25], Lod::Implicit).is_err());
567        assert!(point.sample([0.25, 0.25], Lod::Bias(0.0)).is_err());
568        let stated = make(Filtering::Point).with_footprintless_level(1.0).unwrap();
569        assert_eq!(stated.footprintless_level(), Some(1.0));
570        assert_eq!(stated.sample([0.25, 0.25], Lod::Implicit).unwrap()[0], grey);
571        assert_eq!(stated.sample([0.25, 0.25], Lod::Bias(-1.0)).unwrap()[0], 0.0);
572        assert!(make(Filtering::Point).with_footprintless_level(f32::NAN).is_err());
573    }
574}