iridia-exec 0.5.0

CPU reference executor for lifted Direct3D shaders: run a program on explicit inputs to check a translation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
//! A 2D texture as the executor samples it ([`SampledTexture`], an [`Image`]): its stored levels, the colour transfer
//! applied before filtering, the addressing (with a border's colour), level-of-detail bias and level range its binding's
//! sampler states ([`SamplerUse`]), and the filtering a caller states ([`Filtering`]).
//!
//! The filtering is the caller's policy: what a source states of its sampler's filter may be absent or not modelled, so
//! a caller says how it filters (the base level bilinear, point or bilinear at the nearest level, or trilinear) and
//! reports it. The addressing, the bias and the level range are the binding's data: an axis whose addressing the sampler
//! state leaves unstated is the caller's stated assumption ([`Unstated`]), never a silent wrap; a border reads the colour
//! the state gives it, and a border with none is refused; a stated bias and level range are applied where the level of
//! detail is computed, or named as not applied ([`SampledTexture::unhonoured`]).
//!
//! The level of detail is the quad footprint's, or the level the program states. A sample at an implicit level outside
//! a quad (an evaluation one pixel at a time) has no footprint: it is refused, unless the caller states the level such
//! a sample takes ([`SampledTexture::with_footprintless_level`]).

use iridia_interface::{AddressMode, SamplerState};
use primase_foundations::{
    sampling::Address, sampling::Addressing, sampling::Decoded, sampling::Levels, sampling::Subtexel,
    sampling::Transfer, sampling::footprint_lod,
};

use crate::{Image, Lod};

/// How a texture is filtered: the caller's stated policy.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Filtering {
    /// The base level only, whatever level of detail the program asks for: bilinear in f32 over texels decoded once,
    /// the texel coordinate resolved to `subtexel` precision ([`Decoded::bilinear_at`]).
    BaseBilinear {
        /// The precision the texel coordinate is resolved to.
        subtexel: Subtexel,
    },
    /// The texel the coordinate falls in, of the stored level nearest the level of detail ([`Levels::point`],
    /// [`Levels::nearest_level`]), in f64: a point filter with a point mip filter.
    Point,
    /// Bilinear in f64 ([`Levels::bilinear`]) in the stored level nearest the level of detail: a linear filter with a
    /// point mip filter.
    Bilinear,
    /// The two stored levels about the level of detail mixed, each bilinear in f64 ([`Levels::trilinear`]).
    Trilinear,
}

impl Filtering {
    /// The level of detail [`Filtering::Point`], [`Filtering::Bilinear`] and [`Filtering::Trilinear`] sample at: the
    /// quad footprint's ([`footprint_lod`]: the program's bias, clamped to Direct3D's -16..15.99, then the sampler's
    /// bias, added to the footprint's base-2 logarithm), or the level the program states plus the sampler's bias
    /// (Direct3D's `sample_l` honours the sampler's MipLODBias). Without a footprint, an implicit level is
    /// `footprintless` (plus the program's clamped bias, then the sampler's), refused when the caller states none.
    fn level_of_detail(
        lod: Lod,
        size: [u32; 2],
        sampler_bias: f64,
        footprintless: Option<f32>,
    ) -> Result<f64, String> {
        let [w, h] = size;
        let grad = |g: [[f32; 2]; 2]| g.map(|d| d.map(f64::from));
        let program = |b: f32| f64::from(b.clamp(-16.0, 15.99));
        let without = || {
            footprintless
                .map(f64::from)
                .ok_or_else(|| "an implicit level without a quad's footprint".to_owned())
        };
        Ok(match lod {
            Lod::Gradients(g) => footprint_lod(w, h, grad(g), sampler_bias),
            Lod::GradientsBias(g, b) => footprint_lod(w, h, grad(g), program(b) + sampler_bias),
            Lod::Level(l) => f64::from(l) + sampler_bias,
            Lod::Implicit => without()? + sampler_bias,
            Lod::Bias(b) => without()? + program(b) + sampler_bias,
        })
    }
}

/// What an addressing axis the sampler state leaves unstated is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unstated {
    /// Refused: the binding must state it.
    Refuse,
    /// The caller's stated assumption, which it reports.
    Assume(Address),
}

/// What a binding's sampler gives a texture's sampling: the addressing along u and v, the colour a border reads, and
/// the level-of-detail bias and level range it states.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SamplerUse {
    /// The addressing along u and v.
    pub address: [Address; 2],
    /// The colour a coordinate outside the texture reads along an [`Address::Border`] axis; finite, and stated where an
    /// axis is a border.
    pub border: Option<[f32; 4]>,
    /// Added to a computed level of detail ([`Filtering::Trilinear`]); finite.
    pub mip_lod_bias: Option<f32>,
    /// The least and greatest level of detail the sampler reads: the level, its bias added, is clamped to them; finite
    /// and ordered.
    pub lod: Option<[f32; 2]>,
}

impl SamplerUse {
    /// The addressing `address`, no border colour, no bias and no level range.
    pub fn addressed(address: [Address; 2]) -> SamplerUse {
        SamplerUse {
            address,
            border: None,
            mip_lod_bias: None,
            lod: None,
        }
    }

    /// From a binding's sampler state: its u and v addressing (an unstated axis as `unstated` says), its border colour,
    /// its bias and its level range. Border addressing with no border colour is refused, as is a state that does not
    /// verify ([`SamplerState::verify`]: a colour, bias or range that isn't finite, a reversed range). Whether an axis
    /// was assumed is [`SamplerUse::assumed`].
    pub fn of(state: &SamplerState, unstated: Unstated) -> Result<SamplerUse, String> {
        state.verify()?;
        let axis = |k: usize| -> Result<Address, String> {
            let name = ["u", "v"].get(k).copied().unwrap_or("w");
            Ok(match state.wrap.get(k).copied().flatten() {
                Some(AddressMode::Repeat) => Address::Wrap,
                Some(AddressMode::Mirror) => Address::Mirror,
                Some(AddressMode::Clamp) => Address::Clamp,
                Some(AddressMode::MirrorOnce) => Address::MirrorOnce,
                Some(AddressMode::Border) if state.border.is_some() => Address::Border,
                Some(AddressMode::Border) => {
                    return Err(format!(
                        "border addressing along {name}: the sampler states no border colour"
                    ));
                }
                None => match unstated {
                    Unstated::Assume(a) => a,
                    Unstated::Refuse => {
                        return Err(format!("the sampler states no addressing along {name}"));
                    }
                },
            })
        };
        Ok(SamplerUse {
            address: [axis(0)?, axis(1)?],
            border: state.border,
            mip_lod_bias: state.mip_lod_bias,
            lod: state.lod,
        })
    }

    /// The addressing a filter takes: the axes and the border colour.
    fn addressing(&self) -> Addressing<2> {
        Addressing {
            axes: self.address,
            border: self.border,
        }
    }

    /// `level` clamped to the stated level range, if any.
    fn clamped(&self, level: f64) -> f64 {
        match self.lod {
            Some([low, high]) => level.clamp(f64::from(low), f64::from(high)),
            None => level,
        }
    }

    /// Whether `state` leaves the u or v addressing unstated: what [`SamplerUse::of`] took from [`Unstated`].
    pub fn assumed(state: &SamplerState) -> bool {
        state.wrap[0].is_none() || state.wrap[1].is_none()
    }
}

/// A texture the executor samples (see the module documentation).
#[derive(Clone, Debug)]
pub struct SampledTexture {
    levels: Levels,
    transfer: Transfer,
    sampler: SamplerUse,
    filtering: Filtering,
    /// The level an implicit-level sample without a footprint takes, when the caller states one.
    footprintless: Option<f32>,
    /// The base level decoded once, for [`Filtering::BaseBilinear`].
    base: Option<Decoded>,
}

impl SampledTexture {
    /// `levels` (at least one), read through `transfer`, sampled as `sampler` and `filtering` say; a sampler bias,
    /// border colour or level range that isn't finite, a reversed range, and a border axis with no colour are refused.
    pub fn new(
        levels: Levels,
        transfer: Transfer,
        sampler: SamplerUse,
        filtering: Filtering,
    ) -> Result<SampledTexture, String> {
        if levels.levels.is_empty() {
            return Err("a texture with no stored level".into());
        }
        if let Some(b) = sampler.mip_lod_bias
            && !b.is_finite()
        {
            return Err(format!("sampler bias {b} is not finite"));
        }
        if let Some([low, high]) = sampler.lod
            && (!low.is_finite() || !high.is_finite() || low > high)
        {
            return Err(format!(
                "sampler level range {low}..{high} is not a finite, ordered range"
            ));
        }
        match sampler.border {
            Some(c) if c.iter().any(|v| !v.is_finite()) => {
                return Err(format!("border colour {c:?} is not finite"));
            }
            None if sampler.address.contains(&Address::Border) => {
                return Err("border addressing with no border colour".into());
            }
            _ => {}
        }
        let base = match filtering {
            Filtering::BaseBilinear { .. } => {
                Some(levels.decoded(0, transfer).map_err(|e| format!("level 0: {e}"))?)
            }
            Filtering::Point | Filtering::Bilinear | Filtering::Trilinear => None,
        };
        Ok(SampledTexture {
            levels,
            transfer,
            sampler,
            filtering,
            footprintless: None,
            base,
        })
    }

    /// The same texture with `level` the level of detail of an implicit-level sample that has no quad footprint (an
    /// evaluation one pixel at a time): the caller's stated assumption, which it reports. A level that isn't finite
    /// is refused.
    pub fn with_footprintless_level(mut self, level: f32) -> Result<SampledTexture, String> {
        if !level.is_finite() {
            return Err(format!("footprintless level {level} is not finite"));
        }
        self.footprintless = Some(level);
        Ok(self)
    }

    /// The level an implicit-level sample without a footprint takes, if the caller stated one.
    pub fn footprintless_level(&self) -> Option<f32> {
        self.footprintless
    }

    /// The stored levels.
    pub fn levels(&self) -> &Levels {
        &self.levels
    }

    /// The transfer function the texels are decoded with.
    pub fn transfer(&self) -> Transfer {
        self.transfer
    }

    /// The sampler state the texture is sampled with.
    pub fn sampler(&self) -> SamplerUse {
        self.sampler
    }

    /// The addressing along u and v.
    pub fn address(&self) -> [Address; 2] {
        self.sampler.address
    }

    /// The filtering policy.
    pub fn filtering(&self) -> Filtering {
        self.filtering
    }

    /// The base level's width and height.
    pub fn dimensions(&self) -> [usize; 2] {
        [self.levels.width as usize, self.levels.height as usize]
    }

    /// The bytes it holds: the stored levels and the texels decoded for filtering.
    pub fn decoded_bytes(&self) -> usize {
        self.levels
            .levels
            .iter()
            .map(Vec::len)
            .fold(0usize, usize::saturating_add)
            .saturating_add(self.base.as_ref().map_or(0, Decoded::decoded_bytes))
    }

    /// What of its sampler this sampling does not apply, for the caller to report: under [`Filtering::BaseBilinear`],
    /// which reads the base level whatever the level of detail, a stated bias, and a stated level range that excludes
    /// the base level.
    pub fn unhonoured(&self) -> Option<&'static str> {
        if !matches!(self.filtering, Filtering::BaseBilinear { .. }) {
            return None;
        }
        let bias = self.sampler.mip_lod_bias.is_some_and(|b| b != 0.0);
        let range = self.sampler.lod.is_some_and(|[low, _]| low > 0.0);
        match (bias, range) {
            (true, true) => Some("stated sampler bias and level range not applied (base level)"),
            (true, false) => Some("stated sampler bias not applied (base level)"),
            (false, true) => Some("stated sampler level range not applied (base level)"),
            (false, false) => None,
        }
    }
}

impl Image for SampledTexture {
    fn sample(&self, uv: [f32; 2], lod: Lod) -> Result<[f32; 4], String> {
        let address = self.sampler.addressing();
        let level = || {
            Filtering::level_of_detail(
                lod,
                [self.levels.width, self.levels.height],
                self.sampler.mip_lod_bias.map_or(0.0, f64::from),
                self.footprintless,
            )
            .map(|l| self.sampler.clamped(l))
        };
        let (at, levels, transfer) = ([f64::from(uv[0]), f64::from(uv[1])], &self.levels, self.transfer);
        let v = match (self.filtering, &self.base) {
            (Filtering::BaseBilinear { subtexel }, Some(base)) => {
                return base.bilinear_at(uv, address, subtexel).map_err(|e| e.to_string());
            }
            (Filtering::BaseBilinear { .. }, None) => {
                return Err("a base-level texture whose base was not decoded when it was made".into());
            }
            (Filtering::Point, _) => levels
                .nearest_level(level()?)
                .and_then(|l| levels.point(l, at, address, transfer)),
            (Filtering::Bilinear, _) => levels
                .nearest_level(level()?)
                .and_then(|l| levels.bilinear(l, at, address, transfer)),
            (Filtering::Trilinear, _) => levels.trilinear(at, level()?, address, transfer),
        }
        .map_err(|e| e.to_string())?;
        Ok(v.map(crate::narrow))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn state(u: Option<AddressMode>, v: Option<AddressMode>, bias: Option<f32>) -> SamplerState {
        SamplerState {
            wrap: [u, v, None],
            mip_lod_bias: bias,
            ..SamplerState::default()
        }
    }

    /// A 2 x 1 texture, black then white, one level.
    fn two_texels() -> Levels {
        Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255]).unwrap()
    }

    /// The stated addressing is what samples past the edge read; an unstated axis is the caller's assumption or a
    /// refusal; border addressing and a non-finite bias are refused.
    #[test]
    fn the_sampler_states_the_addressing() {
        let clamp = SamplerUse::of(
            &state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), None),
            Unstated::Refuse,
        )
        .unwrap();
        assert_eq!(clamp.address, [Address::Clamp; 2]);
        let base = Filtering::BaseBilinear {
            subtexel: Subtexel::D3d11,
        };
        let clamped = SampledTexture::new(two_texels(), Transfer::Stored, clamp, base).unwrap();
        let wrapped = SampledTexture::new(
            two_texels(),
            Transfer::Stored,
            SamplerUse::addressed([Address::Wrap; 2]),
            base,
        )
        .unwrap();
        // u = 0: half a texel left of the first centre; clamped reads black, wrapped mixes in the white texel
        assert_eq!(clamped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.0);
        assert_eq!(wrapped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.5);
        assert!(SamplerUse::of(&state(None, Some(AddressMode::Clamp), None), Unstated::Refuse).is_err());
        let assumed = SamplerUse::of(
            &state(None, Some(AddressMode::Clamp), None),
            Unstated::Assume(Address::Wrap),
        )
        .unwrap();
        assert_eq!(assumed.address, [Address::Wrap, Address::Clamp]);
        assert!(SamplerUse::assumed(&state(None, Some(AddressMode::Clamp), None)));
        // a border with no stated colour is refused
        assert!(
            SamplerUse::of(
                &state(Some(AddressMode::Border), Some(AddressMode::Clamp), None),
                Unstated::Refuse
            )
            .is_err()
        );
        assert!(
            SamplerUse::of(
                &state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), Some(f32::NAN)),
                Unstated::Refuse
            )
            .is_err()
        );
    }

    /// A stated border colour is what samples past a border axis read: black then white, sampled past the left edge
    /// with borders 0 and 1; bilinear at the edge mixes half the border with half the black texel. A colour that isn't
    /// finite is refused, and so is a border axis with no colour.
    #[test]
    fn a_stated_border_colour_is_read_past_the_edge() {
        let bordered = |colour: [f32; 4]| {
            let s = SamplerState {
                border: Some(colour),
                ..state(Some(AddressMode::Border), Some(AddressMode::Clamp), None)
            };
            let sampler = SamplerUse::of(&s, Unstated::Refuse).unwrap();
            assert_eq!(sampler.address, [Address::Border, Address::Clamp]);
            sampler
        };
        for filtering in [
            Filtering::BaseBilinear {
                subtexel: Subtexel::D3d11,
            },
            Filtering::Bilinear,
        ] {
            let zero =
                SampledTexture::new(two_texels(), Transfer::Stored, bordered([0.0; 4]), filtering).unwrap();
            let one =
                SampledTexture::new(two_texels(), Transfer::Stored, bordered([1.0; 4]), filtering).unwrap();
            // a quarter texel past the left edge: wholly the border
            let past = |t: &SampledTexture| t.sample([-0.25, 0.5], Lod::Level(0.0)).unwrap();
            assert_eq!(past(&zero), [0.0; 4]);
            assert_eq!(past(&one), [1.0; 4]);
            // the left edge: half the border, half the black texel (alpha 1)
            let edge = |t: &SampledTexture| t.sample([0.0, 0.5], Lod::Level(0.0)).unwrap();
            assert_eq!(edge(&zero), [0.0, 0.0, 0.0, 0.5]);
            assert_eq!(edge(&one), [0.5, 0.5, 0.5, 1.0]);
        }
        let nan = SamplerState {
            border: Some([f32::NAN; 4]),
            ..state(Some(AddressMode::Border), Some(AddressMode::Clamp), None)
        };
        assert!(SamplerUse::of(&nan, Unstated::Refuse).is_err());
        let mut colourless = SamplerUse::addressed([Address::Border; 2]);
        assert!(
            SampledTexture::new(two_texels(), Transfer::Stored, colourless, Filtering::Bilinear).is_err()
        );
        colourless.border = Some([0.0; 4]);
        assert!(SampledTexture::new(two_texels(), Transfer::Stored, colourless, Filtering::Bilinear).is_ok());
    }

    /// A stated level range clamps the level of detail after the bias: levels 0 (black) and 1 (white), a range of
    /// 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
    /// that excludes it as not applied. A reversed range is refused.
    #[test]
    fn a_stated_level_range_clamps_the_level() {
        let levels = Levels {
            width: 2,
            height: 2,
            levels: vec![[0, 0, 0, 255].repeat(4), vec![255; 4]],
        };
        let make = |lod: Option<[f32; 2]>, filtering| {
            SampledTexture::new(
                levels.clone(),
                Transfer::Stored,
                SamplerUse {
                    lod,
                    ..SamplerUse::addressed([Address::Wrap; 2])
                },
                filtering,
            )
            .unwrap()
        };
        let base_only = make(Some([0.0, 0.0]), Filtering::Trilinear);
        assert_eq!(base_only.sample([0.5, 0.5], Lod::Level(1.0)).unwrap()[0], 0.0);
        let first = make(Some([1.0, 1.0]), Filtering::Point);
        assert_eq!(first.sample([0.5, 0.5], Lod::Level(0.0)).unwrap()[0], 1.0);
        let unclamped = make(None, Filtering::Point);
        assert_eq!(unclamped.sample([0.5, 0.5], Lod::Level(0.0)).unwrap()[0], 0.0);
        let base = Filtering::BaseBilinear {
            subtexel: Subtexel::D3d11,
        };
        assert_eq!(
            make(Some([1.0, 1.0]), base).unhonoured(),
            Some("stated sampler level range not applied (base level)")
        );
        assert_eq!(make(Some([0.0, 0.0]), base).unhonoured(), None);
        let reversed = SamplerUse {
            lod: Some([2.0, 1.0]),
            ..SamplerUse::addressed([Address::Wrap; 2])
        };
        assert!(SampledTexture::new(levels.clone(), Transfer::Stored, reversed, Filtering::Point).is_err());
    }

    /// A stated bias moves the footprint's level and a stated level under trilinear filtering; the base level names
    /// it as not applied; an implicit level is refused.
    #[test]
    fn a_stated_bias_is_applied_or_named() {
        // two levels: 2 x 2 black, 1 x 1 white
        let levels = Levels {
            width: 2,
            height: 2,
            levels: vec![[0, 0, 0, 255].repeat(4), vec![255; 4]],
        };
        let make = |bias: Option<f32>, filtering| {
            SampledTexture::new(
                levels.clone(),
                Transfer::Stored,
                SamplerUse {
                    mip_lod_bias: bias,
                    ..SamplerUse::addressed([Address::Wrap; 2])
                },
                filtering,
            )
            .unwrap()
        };
        // a footprint of half a texel: level -1, clamped to 0 (black); a bias of 1 more reaches level 0 still, 2 more
        // reaches level 1 (white)
        let footprint = Lod::Gradients([[0.25, 0.0], [0.0, 0.25]]);
        let plain = make(None, Filtering::Trilinear);
        assert_eq!(plain.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
        let biased = make(Some(2.0), Filtering::Trilinear);
        assert_eq!(biased.sample([0.5, 0.5], footprint).unwrap()[0], 1.0);
        assert_eq!(biased.unhonoured(), None);
        // a stated level takes the sampler's bias too: level -1 biased by 2 is level 1 (white); unbiased, level 0
        assert_eq!(biased.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 1.0);
        assert_eq!(plain.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 0.0);
        assert_eq!(plain.sample([0.5, 0.5], Lod::Level(1.0)).unwrap()[0], 1.0);
        assert!(plain.sample([0.5, 0.5], Lod::Implicit).is_err());
        let base = make(
            Some(2.0),
            Filtering::BaseBilinear {
                subtexel: Subtexel::D3d11,
            },
        );
        assert_eq!(base.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
        assert_eq!(
            base.unhonoured(),
            Some("stated sampler bias not applied (base level)")
        );
    }

    /// Point and bilinear filtering read the stored level nearest the level of detail (a half rounds up); an implicit
    /// level without a footprint is refused unless the caller states the level it takes, and a program's bias then
    /// adds to that level.
    #[test]
    fn nearest_level_filters_and_a_stated_footprintless_level() {
        // two levels: 2 x 2 with columns black and white, 1 x 1 grey
        let levels = Levels {
            width: 2,
            height: 2,
            levels: vec![[[0, 0, 0, 255], [255; 4]].concat().repeat(2), vec![128; 4]],
        };
        let make = |filtering| {
            SampledTexture::new(
                levels.clone(),
                Transfer::Stored,
                SamplerUse::addressed([Address::Wrap; 2]),
                filtering,
            )
            .unwrap()
        };
        let grey = 128.0 / 255.0;
        let point = make(Filtering::Point);
        assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.0)).unwrap()[0], 0.0);
        assert_eq!(point.sample([0.75, 0.25], Lod::Level(0.49)).unwrap()[0], 1.0);
        assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.5)).unwrap()[0], grey);
        let bilinear = make(Filtering::Bilinear);
        assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.4)).unwrap()[0], 0.5);
        assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.6)).unwrap()[0], grey);
        assert!(point.sample([0.25, 0.25], Lod::Implicit).is_err());
        assert!(point.sample([0.25, 0.25], Lod::Bias(0.0)).is_err());
        let stated = make(Filtering::Point).with_footprintless_level(1.0).unwrap();
        assert_eq!(stated.footprintless_level(), Some(1.0));
        assert_eq!(stated.sample([0.25, 0.25], Lod::Implicit).unwrap()[0], grey);
        assert_eq!(stated.sample([0.25, 0.25], Lod::Bias(-1.0)).unwrap()[0], 0.0);
        assert!(make(Filtering::Point).with_footprintless_level(f32::NAN).is_err());
    }
}