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