Skip to main content

iridia_exec/
views.rs

1//! Resource views bound by slot ([`Views`]), answering a program's requests. Where nothing is bound or an address falls
2//! outside a view, they answer as their policy states ([`Absent`]): as Direct3D answers ([`Absent::Zero`], the rules
3//! below), or by refusing, naming the slot or address ([`Absent::Refuse`]: an evaluation runs on no value it was not given).
4//! The rules are the D3D11.3 functional specification's, stated here once:
5//!
6//! - **Nothing bound.** A sample from a slot with no texture bound returns 0 in all components (7.18.17), and so does a
7//!   load ("Fetching from t# that has nothing bound to it returns 0 for all components", 22.4.6), a structured read
8//!   (22.4.12) and a size query (22.4.14, `resinfo`). A buffer at an empty slot has no elements.
9//! - **Out of range.** A load whose address or level is outside the view returns 0 in all components (22.4.6's
10//!   out-of-bounds behaviour; every view here has all four components, so no missing-component default arises), a
11//!   typed buffer's element past its end too, and a structured read of an element past the end returns 0 (22.4.12). A
12//!   structured read whose byte offset is past its element is undefined there (22.4.12) and refused here.
13//! - **Sampling.** A view holds one level, so every level of detail reads it; a sample is filtered as its sampler slot
14//!   states ([`ViewSampler`]): the texel it falls in, or linear between the texels about it (four in 2D, eight in a
15//!   volume), each axis addressed (a border reads the slot's border colour; a border with none is refused). A sampler
16//!   slot nothing states is refused, never assumed. An image the caller samples
17//!   itself ([`View::Image`]) is sampled by its own policy at the level of detail the program states.
18//!
19//! What a view is not asked for is refused by name: a gather, a comparison, a texel offset, a cube or 1D texture, a
20//! layer other than 0 of a 2D view (any layer of a caller's image), a multisample or unordered-access load, a store or
21//! an atomic (their effect is the caller's).
22
23use std::collections::BTreeMap;
24use std::sync::Arc;
25
26use primase_foundations::{sampling::Address, sampling::Addressing, sampling::Decoded, sampling::Subtexel};
27
28use crate::{Dim, ImageLoad, ImageQuery, ImageSample, Load, Lod, QueryKind, Sample, Sampler};
29
30/// A texture the caller samples itself (its footprint, addressing, filtering and transfer are its own policy), by the
31/// level of detail the program states ([`crate::Lod`]).
32pub trait Image: Send + Sync + std::fmt::Debug {
33    /// The texel at `uv` at level of detail `lod`, or an error that stops the evaluation.
34    fn sample(&self, uv: [f32; 2], lod: Lod) -> Result<[f32; 4], String>;
35}
36
37/// One bound view.
38#[derive(Clone, Debug)]
39pub enum View {
40    /// One value everywhere: every sample, and the load of its one texel (a 1 x 1 x 1 texture).
41    Uniform([f32; 4]),
42    /// One value at every texel of a texture whose size is not stated: every sample, and every load the value's bits
43    /// (as a float view returns them). A size query is refused.
44    Constant([f32; 4]),
45    /// A 2D float texture of one level (one slice).
46    Texture2d(Decoded),
47    /// A 3D float texture of one level.
48    Texture3d(Decoded),
49    /// A 2D texture the caller samples ([`Image`]): samples only, of no array layer.
50    Image(Arc<dyn Image>),
51    /// A typed buffer of four 32-bit lanes per element.
52    Typed(Arc<[[u32; 4]]>),
53    /// A structured buffer: elements of `stride` bytes, as 32-bit words.
54    Structured {
55        /// Bytes per element.
56        stride: u32,
57        /// The buffer's 32-bit words, element after element.
58        words: Vec<u32>,
59    },
60}
61
62/// How a sampler slot filters a view.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum ViewFilter {
65    /// The texel a coordinate falls in.
66    Point,
67    /// Linear between the texels about a coordinate, the texel coordinate resolved to `subtexel` precision.
68    Linear {
69        /// The precision the texel coordinate is resolved to.
70        subtexel: Subtexel,
71    },
72}
73
74/// A sampler slot's state: its filter, the addressing along u, v and w, and the colour a border reads.
75#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct ViewSampler {
77    /// How the sampler filters.
78    pub filter: ViewFilter,
79    /// The addressing along u, v and w.
80    pub address: [Address; 3],
81    /// The colour a coordinate outside the texture reads along an [`Address::Border`] axis, where the slot states one;
82    /// a sample across a border axis with none is refused.
83    pub border: Option<[f32; 4]>,
84}
85
86/// What a request reads where the views hold nothing for it: a slot nothing is bound to, or an address or level outside
87/// its view (a structured read's byte offset past its element is refused either way).
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub enum Absent {
90    /// Zero, as Direct3D reads it (the module documentation); a buffer's length is stated, so the executor answers its
91    /// reads past the end with zero itself.
92    #[default]
93    Zero,
94    /// Refused, naming the slot or the address; no buffer's length is stated, so every read reaches the view.
95    Refuse,
96}
97
98/// The views and sampler states bound by slot, and what a request reads where they hold nothing (see the module
99/// documentation). A program reads them through `&Views` (the views are only read, so one table serves every thread).
100#[derive(Clone, Debug, Default)]
101pub struct Views {
102    /// The views by bind slot (`t<n>` or `u<n>`).
103    pub views: BTreeMap<u32, View>,
104    /// The sampler states by bind slot (`s<n>`).
105    pub samplers: BTreeMap<u32, ViewSampler>,
106    /// What a read does where nothing is bound or addressed.
107    pub absent: Absent,
108}
109
110impl Views {
111    /// `views` with no sampler states, refusing what they hold nothing for ([`Absent::Refuse`]): a grid evaluation's table.
112    pub fn refusing(views: BTreeMap<u32, View>) -> Self {
113        Views {
114            views,
115            samplers: BTreeMap::new(),
116            absent: Absent::Refuse,
117        }
118    }
119
120    fn sampler(&self, slot: u32) -> Result<ViewSampler, String> {
121        self.samplers
122            .get(&slot)
123            .copied()
124            .ok_or_else(|| format!("s{slot}: no sampler state bound"))
125    }
126
127    /// What a request reads where the views hold nothing for it: zero, or `why` refused.
128    fn absent<T: Default>(&self, why: impl FnOnce() -> String) -> Result<T, String> {
129        match self.absent {
130            Absent::Zero => Ok(T::default()),
131            Absent::Refuse => Err(why()),
132        }
133    }
134
135    /// A sample of view `texture` at `coord` (`dim`'s axes) through sampler slot `sampler`, at `lod`.
136    fn filtered(
137        &self,
138        texture: u32,
139        sampler: u32,
140        dim: Dim,
141        coord: [f32; 3],
142        lod: Lod,
143    ) -> Result<[f32; 4], String> {
144        let state = || self.sampler(sampler);
145        let error = |e: primase_foundations::sampling::Error| format!("t{texture}: {e}");
146        match (self.views.get(&texture), dim) {
147            (None, _) => self.absent(|| format!("t{texture} has no texture")),
148            (Some(View::Uniform(c) | View::Constant(c)), _) => Ok(*c),
149            (Some(View::Image(image)), Dim::D2) => image.sample([coord[0], coord[1]], lod),
150            (Some(View::Texture2d(image)), Dim::D2) => {
151                let state = state()?;
152                let (uv, address) = (
153                    [coord[0], coord[1]],
154                    Addressing {
155                        axes: [state.address[0], state.address[1]],
156                        border: state.border,
157                    },
158                );
159                match state.filter {
160                    ViewFilter::Point => image.point(uv, address),
161                    ViewFilter::Linear { subtexel } => image.bilinear_at(uv, address, subtexel),
162                }
163                .map_err(error)
164            }
165            (Some(View::Texture3d(image)), Dim::D3) => {
166                let state = state()?;
167                let address = Addressing {
168                    axes: state.address,
169                    border: state.border,
170                };
171                match state.filter {
172                    ViewFilter::Point => image.point_3d(coord, address),
173                    ViewFilter::Linear { subtexel } => image.linear_3d(coord, address, subtexel),
174                }
175                .map_err(error)
176            }
177            (Some(View::Typed(_) | View::Structured { .. }), _) => {
178                Err(format!("t{texture}: a sample of a buffer"))
179            }
180            (Some(View::Texture2d(_) | View::Texture3d(_) | View::Image(_)), dim) => Err(format!(
181                "t{texture}: a {dim:?} sample of a view of another dimension"
182            )),
183        }
184    }
185}
186
187impl Sampler for &Views {
188    fn sample(&mut self, r: Sample) -> Result<[f32; 4], String> {
189        match self.views.get(&r.texture) {
190            // a 2D view has one layer: an array's other layers are not bound here
191            Some(View::Texture2d(_)) if r.layer.is_some_and(|l| l.round_ties_even() != 0.0) => {
192                return Err(format!("t{}: layer {:?} of a 2D view", r.texture, r.layer));
193            }
194            Some(View::Image(_)) if r.layer.is_some() => {
195                return Err(format!("t{}: an array layer of an image", r.texture));
196            }
197            _ => {}
198        }
199        self.filtered(r.texture, r.sampler, Dim::D2, [r.uv[0], r.uv[1], 0.0], r.lod)
200    }
201
202    fn sample_image(&mut self, r: ImageSample<u32>) -> Result<[f32; 4], String> {
203        if r.gather.is_some() || r.depth_ref.is_some() || r.depth || r.offset != [0; 3] {
204            return Err(format!(
205                "t{}: a gather, a comparison or a texel offset, which these views do not answer",
206                r.texture
207            ));
208        }
209        match r.dim {
210            Dim::D2 => self.sample(Sample {
211                texture: r.texture,
212                sampler: r.sampler,
213                uv: [r.coord[0], r.coord[1]],
214                layer: r.layer,
215                lod: Lod::from(r.lod),
216            }),
217            Dim::D3 => self.filtered(r.texture, r.sampler, Dim::D3, r.coord, Lod::from(r.lod)),
218            dim => Err(format!(
219                "t{}: a {dim:?} sample, which these views do not answer",
220                r.texture
221            )),
222        }
223    }
224
225    fn load(&mut self, r: Load) -> Result<[u32; 4], String> {
226        let [x, y, z] = r.texel.map(i64::from);
227        let bits = |c: [f32; 4]| c.map(f32::to_bits);
228        let outside = || {
229            format!(
230                "t{}: texel {:?} of level {} outside the view",
231                r.texture, r.texel, r.mip
232            )
233        };
234        match self.views.get(&r.texture) {
235            None => self.absent(|| format!("t{} has no texture", r.texture)),
236            Some(View::Uniform(c)) if (x, y, z, r.mip) == (0, 0, 0, 0) => Ok(bits(*c)),
237            Some(View::Uniform(_)) => self.absent(outside),
238            Some(View::Constant(c)) => Ok(bits(*c)),
239            Some(View::Texture2d(image) | View::Texture3d(image)) if r.mip == 0 => {
240                match image.texel(x, y, z) {
241                    Some(texel) => Ok(bits(texel)),
242                    None => self.absent(outside),
243                }
244            }
245            Some(View::Texture2d(_) | View::Texture3d(_)) => self.absent(outside),
246            Some(View::Image(_)) => Err(format!("t{}: a load of an image", r.texture)),
247            // a buffer is addressed by its element; it has no levels
248            Some(View::Typed(elements)) => match usize::try_from(x).ok().and_then(|i| elements.get(i)) {
249                Some(element) => Ok(*element),
250                None => {
251                    self.absent(|| format!("t{}: element {} of {}", r.texture, r.texel[0], elements.len()))
252                }
253            },
254            Some(View::Structured { .. }) => {
255                Err(format!("t{}: a texel load of a structured buffer", r.texture))
256            }
257        }
258    }
259
260    fn load_image(&mut self, r: ImageLoad<u32>) -> Result<[u32; 4], String> {
261        if r.storage || r.sample.is_some() {
262            return Err(format!(
263                "t{}: an unordered-access or multisample load, which these views do not answer",
264                r.texture
265            ));
266        }
267        if !matches!(r.dim, Dim::D2 | Dim::D3) {
268            return Err(format!(
269                "t{}: a {:?} load, which these views do not answer",
270                r.texture, r.dim
271            ));
272        }
273        self.load(Load {
274            texture: r.texture,
275            texel: [r.texel[0], r.texel[1], r.layer.unwrap_or(r.texel[2])],
276            mip: r.mip.unwrap_or(0),
277        })
278    }
279
280    fn structured(&mut self, slot: u32, element: u32, offset: u32) -> Result<u32, String> {
281        match self.views.get(&slot) {
282            None => self.absent(|| format!("t{slot} has no texture")),
283            Some(View::Structured { stride, .. }) if offset.saturating_add(4) > *stride => Err(format!(
284                "t{slot}: a structured read at byte {offset} of an element of {stride} bytes (undefined)"
285            )),
286            Some(View::Structured { stride, words }) => {
287                let byte = u64::from(element)
288                    .saturating_mul(u64::from(*stride))
289                    .saturating_add(u64::from(offset));
290                match usize::try_from(byte / 4).ok().and_then(|w| words.get(w)) {
291                    Some(word) => Ok(*word),
292                    None => self.absent(|| format!("t{slot}: element {element} past the end")),
293                }
294            }
295            Some(_) => Err(format!(
296                "t{slot}: a structured read of a view that is not a structured buffer"
297            )),
298        }
299    }
300
301    fn buffer_elements(&mut self, slot: u32) -> Option<u32> {
302        if self.absent == Absent::Refuse {
303            return None;
304        }
305        match self.views.get(&slot) {
306            None => Some(0),
307            Some(View::Typed(e)) => u32::try_from(e.len()).ok(),
308            Some(View::Structured { stride, words }) if *stride > 0 => u32::try_from(
309                (words.len() as u64)
310                    .saturating_mul(4)
311                    .checked_div(u64::from(*stride))?,
312            )
313            .ok(),
314            Some(_) => None,
315        }
316    }
317
318    fn query_image(&mut self, r: ImageQuery<u32>) -> Result<[u32; 4], String> {
319        let size = match self.views.get(&r.texture) {
320            None => return self.absent(|| format!("t{} has no texture", r.texture)),
321            Some(View::Uniform(_)) => [1, 1, 1],
322            Some(View::Texture2d(image) | View::Texture3d(image)) => {
323                let [w, h] = image.dimensions();
324                [w, h, image.depth()].map(|n| u32::try_from(n).unwrap_or(u32::MAX))
325            }
326            Some(View::Constant(_) | View::Image(_)) => {
327                return Err(format!(
328                    "t{}: a size query of a texture whose size is not stated",
329                    r.texture
330                ));
331            }
332            Some(View::Typed(_) | View::Structured { .. }) => {
333                return Err(format!("t{}: a texture query of a buffer", r.texture));
334            }
335        };
336        match r.kind {
337            QueryKind::Size if r.level.unwrap_or(0) == 0 => Ok([size[0], size[1], size[2], 0]),
338            // a level past the view's one: as an empty slot (resinfo)
339            QueryKind::Size => {
340                self.absent(|| format!("t{}: level {:?} past the view's one", r.texture, r.level))
341            }
342            QueryKind::Levels | QueryKind::Layers | QueryKind::Samples => Ok([1, 0, 0, 0]),
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::{ImageLod, Lod};
351
352    /// A 2 x 2 texture, texel (x, y) = (x, y, 0, 1), at t3; a 2 x 2 x 2 volume, texel (x, y, z) = (x, y, z, 1), at t4.
353    fn views(filter: ViewFilter, address: Address) -> Views {
354        let plane = (0..4)
355            .map(|i| [(i % 2) as f32, (i / 2) as f32, 0.0, 1.0])
356            .collect();
357        let volume = (0..8)
358            .map(|i| [(i % 2) as f32, (i / 2 % 2) as f32, (i / 4) as f32, 1.0])
359            .collect();
360        Views {
361            views: BTreeMap::from([
362                (3, View::Texture2d(Decoded::from_texels(2, 2, plane).unwrap())),
363                (4, View::Texture3d(Decoded::volume(2, 2, 2, volume).unwrap())),
364                (5, View::Typed(Arc::from([[7; 4], [8; 4]]))),
365                (
366                    6,
367                    View::Structured {
368                        stride: 8,
369                        words: vec![10, 11, 12, 13],
370                    },
371                ),
372                (7, View::Uniform([0.25; 4])),
373            ]),
374            samplers: BTreeMap::from([(
375                0,
376                ViewSampler {
377                    filter,
378                    address: [address; 3],
379                    border: None,
380                },
381            )]),
382            absent: Absent::Zero,
383        }
384    }
385
386    fn sample(mut v: &Views, texture: u32, uv: [f32; 2]) -> Result<[f32; 4], String> {
387        v.sample(Sample {
388            texture,
389            sampler: 0,
390            uv,
391            layer: None,
392            lod: Lod::Implicit,
393        })
394    }
395
396    fn sample_3d(mut v: &Views, texture: u32, coord: [f32; 3]) -> Result<[f32; 4], String> {
397        v.sample_image(ImageSample {
398            texture,
399            sampler: 0,
400            dim: Dim::D3,
401            coord,
402            layer: None,
403            lod: ImageLod::Implicit,
404            offset: [0; 3],
405            gather: None,
406            depth_ref: None,
407            depth: false,
408        })
409    }
410
411    fn load(mut v: &Views, texture: u32, texel: [i32; 3], mip: i32) -> Result<[u32; 4], String> {
412        v.load(Load { texture, texel, mip })
413    }
414
415    /// A border axis reads the slot's border colour past the edge: point past the left edge of t3 is the colour, inside
416    /// the texel; linear at the edge mixes half the colour with half texel (0, 0) = (0, 0, 0, 1); a volume past its
417    /// last slice is the colour. A border slot with no colour is refused.
418    #[test]
419    fn a_border_reads_the_slot_colour() {
420        let mut v = views(ViewFilter::Point, Address::Border);
421        assert!(sample(&v, 3, [-0.25, 0.25]).is_err());
422        let colour = |v: &mut Views, c: [f32; 4]| {
423            if let Some(s) = v.samplers.get_mut(&0) {
424                s.border = Some(c);
425            }
426        };
427        colour(&mut v, [1.0; 4]);
428        assert_eq!(sample(&v, 3, [-0.25, 0.25]), Ok([1.0; 4]));
429        assert_eq!(sample(&v, 3, [0.75, 0.25]), Ok([1.0, 0.0, 0.0, 1.0]));
430        assert_eq!(sample_3d(&v, 4, [0.25, 0.25, 1.25]), Ok([1.0; 4]));
431        let mut l = views(
432            ViewFilter::Linear {
433                subtexel: Subtexel::Exact,
434            },
435            Address::Border,
436        );
437        colour(&mut l, [0.0; 4]);
438        assert_eq!(sample(&l, 3, [0.0, 0.25]), Ok([0.0, 0.0, 0.0, 0.5]));
439        colour(&mut l, [1.0; 4]);
440        assert_eq!(sample(&l, 3, [0.0, 0.25]), Ok([0.5, 0.5, 0.5, 1.0]));
441    }
442
443    #[test]
444    fn samples_are_filtered_as_their_sampler_states() {
445        let point = ViewFilter::Point;
446        let linear = ViewFilter::Linear {
447            subtexel: Subtexel::Exact,
448        };
449        let v = &views(point, Address::Clamp);
450        assert_eq!(sample(v, 3, [0.75, 0.25]), Ok([1.0, 0.0, 0.0, 1.0]));
451        assert_eq!(sample(v, 3, [1.75, -3.0]), Ok([1.0, 0.0, 0.0, 1.0]));
452        assert_eq!(sample_3d(v, 4, [0.25, 0.75, 0.75]), Ok([0.0, 1.0, 1.0, 1.0]));
453        let w = &views(point, Address::Wrap);
454        assert_eq!(sample(w, 3, [-0.25, 0.25]), Ok([1.0, 0.0, 0.0, 1.0]));
455        let l = &views(linear, Address::Clamp);
456        assert_eq!(sample(l, 3, [0.5, 0.5]), Ok([0.5, 0.5, 0.0, 1.0]));
457        assert_eq!(sample_3d(l, 4, [0.5; 3]), Ok([0.5, 0.5, 0.5, 1.0]));
458        // a uniform view is its value, whatever the coordinate, dimension or layer
459        assert_eq!(sample(l, 7, [9.0, -9.0]), Ok([0.25; 4]));
460        assert_eq!(sample_3d(l, 7, [0.5; 3]), Ok([0.25; 4]));
461    }
462
463    #[test]
464    fn what_is_not_bound_reads_zero() {
465        let mut v = &views(ViewFilter::Point, Address::Clamp);
466        assert_eq!(sample(v, 9, [0.5, 0.5]), Ok([0.0; 4]));
467        assert_eq!(sample_3d(v, 9, [0.5; 3]), Ok([0.0; 4]));
468        assert_eq!(load(v, 9, [0, 0, 0], 0), Ok([0; 4]));
469        assert_eq!(v.structured(9, 0, 0), Ok(0));
470        assert_eq!(v.buffer_elements(9), Some(0));
471        let query = ImageQuery {
472            texture: 9,
473            dim: Dim::D2,
474            kind: QueryKind::Size,
475            level: None,
476        };
477        assert_eq!(v.query_image(query), Ok([0; 4]));
478        assert_eq!(
479            v.query_image(ImageQuery { texture: 4, ..query }),
480            Ok([2, 2, 2, 0])
481        );
482    }
483
484    #[test]
485    fn a_load_outside_its_view_reads_zero() {
486        let mut v = &views(ViewFilter::Point, Address::Clamp);
487        let one = 1f32.to_bits();
488        assert_eq!(load(v, 3, [1, 1, 0], 0), Ok([one, one, 0, one]));
489        assert_eq!(load(v, 3, [2, 0, 0], 0), Ok([0; 4]));
490        assert_eq!(load(v, 3, [0, -1, 0], 0), Ok([0; 4]));
491        assert_eq!(load(v, 3, [0, 0, 1], 0), Ok([0; 4]), "a layer past the view");
492        assert_eq!(load(v, 3, [0, 0, 0], 1), Ok([0; 4]), "a level past the view");
493        assert_eq!(load(v, 4, [1, 0, 1], 0), Ok([one, 0, one, one]));
494        assert_eq!(load(v, 4, [0, 0, 2], 0), Ok([0; 4]));
495        assert_eq!(load(v, 5, [1, 0, 0], 0), Ok([8; 4]));
496        assert_eq!(load(v, 5, [2, 0, 0], 0), Ok([0; 4]));
497        assert_eq!(load(v, 7, [0, 0, 0], 0), Ok([0.25f32.to_bits(); 4]));
498        assert_eq!(load(v, 7, [1, 0, 0], 0), Ok([0; 4]));
499        // a structured element past the end is 0; a byte offset past the element is undefined, refused
500        assert_eq!(v.structured(6, 1, 4), Ok(13));
501        assert_eq!(v.structured(6, 2, 0), Ok(0));
502        assert!(v.structured(6, 0, 8).is_err());
503        assert_eq!(v.buffer_elements(6), Some(2));
504        assert_eq!(v.buffer_elements(5), Some(2));
505    }
506
507    #[test]
508    fn what_the_views_do_not_answer_is_refused() {
509        let mut v = &views(ViewFilter::Point, Address::Clamp);
510        // no sampler state at s1
511        assert!(
512            v.sample(Sample {
513                texture: 3,
514                sampler: 1,
515                uv: [0.5; 2],
516                layer: None,
517                lod: Lod::Implicit,
518            })
519            .is_err()
520        );
521        // a 2D view sampled as a volume and the other way; a buffer sampled
522        assert!(sample_3d(v, 3, [0.5; 3]).is_err());
523        assert!(sample(v, 4, [0.5; 2]).is_err());
524        assert!(sample(v, 5, [0.5; 2]).is_err());
525        // a layer other than 0 of a 2D view
526        assert!(
527            v.sample(Sample {
528                texture: 3,
529                sampler: 0,
530                uv: [0.5; 2],
531                layer: Some(1.0),
532                lod: Lod::Implicit,
533            })
534            .is_err()
535        );
536        let gather = ImageSample {
537            texture: 3,
538            sampler: 0,
539            dim: Dim::D2,
540            coord: [0.5; 3],
541            layer: None,
542            lod: ImageLod::Implicit,
543            offset: [0; 3],
544            gather: Some(0),
545            depth_ref: None,
546            depth: false,
547        };
548        assert!(v.sample_image(gather).is_err());
549        assert!(
550            v.sample_image(ImageSample {
551                gather: None,
552                offset: [1, 0, 0],
553                ..gather
554            })
555            .is_err()
556        );
557        assert!(v.structured(3, 0, 0).is_err());
558        assert!(
559            v.load(Load {
560                texture: 6,
561                texel: [0; 3],
562                mip: 0
563            })
564            .is_err()
565        );
566    }
567
568    /// An image that answers its coordinate and the level of detail it was asked at.
569    #[derive(Debug)]
570    struct Echo;
571    impl Image for Echo {
572        fn sample(&self, uv: [f32; 2], lod: Lod) -> Result<[f32; 4], String> {
573            let level = match lod {
574                Lod::Level(l) => l,
575                _ => -1.0,
576            };
577            Ok([uv[0], uv[1], level, 1.0])
578        }
579    }
580
581    /// A grid evaluation's table ([`Views::refusing`]): a constant answers every sample and load, an image its own samples at the
582    /// level the program states, a buffer its elements; what it holds nothing for is refused by name, and no buffer's
583    /// length is stated (the executor would answer zero past it).
584    #[test]
585    fn a_refusing_table_refuses_what_it_does_not_hold() {
586        let mut v = &Views::refusing(BTreeMap::from([
587            (0, View::Constant([0.5; 4])),
588            (1, View::Image(Arc::new(Echo))),
589            (2, View::Typed(Arc::from([[1, 2, 3, 4]]))),
590        ]));
591        assert_eq!(sample(v, 0, [9.0, 9.0]), Ok([0.5; 4]));
592        assert_eq!(load(v, 0, [5, 6, 0], 2), Ok([0.5f32.to_bits(); 4]));
593        let level = ImageSample {
594            texture: 1,
595            sampler: 0,
596            dim: Dim::D2,
597            coord: [0.25, 0.75, 0.0],
598            layer: None,
599            lod: ImageLod::Level(2.0),
600            offset: [0; 3],
601            gather: None,
602            depth_ref: None,
603            depth: false,
604        };
605        assert_eq!(v.sample_image(level), Ok([0.25, 0.75, 2.0, 1.0]));
606        assert_eq!(
607            v.sample(Sample {
608                texture: 1,
609                sampler: 0,
610                uv: [0.5; 2],
611                layer: Some(0.0),
612                lod: Lod::Implicit,
613            }),
614            Err("t1: an array layer of an image".into())
615        );
616        assert_eq!(load(v, 1, [0; 3], 0), Err("t1: a load of an image".into()));
617        assert_eq!(load(v, 2, [0; 3], 0), Ok([1, 2, 3, 4]));
618        assert_eq!(load(v, 2, [1, 0, 0], 0), Err("t2: element 1 of 1".into()));
619        assert_eq!(sample(v, 9, [0.5; 2]), Err("t9 has no texture".into()));
620        assert_eq!(load(v, 9, [0; 3], 0), Err("t9 has no texture".into()));
621        assert_eq!(v.structured(9, 0, 0), Err("t9 has no texture".into()));
622        assert_eq!(v.buffer_elements(2), None);
623        let query = ImageQuery {
624            texture: 0,
625            dim: Dim::D2,
626            kind: QueryKind::Size,
627            level: None,
628        };
629        assert!(v.query_image(query).is_err());
630        assert!(v.query_image(ImageQuery { texture: 9, ..query }).is_err());
631    }
632}