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