1use 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
30pub trait Image: Send + Sync + std::fmt::Debug {
33 fn sample(&self, uv: [f32; 2], lod: Lod) -> Result<[f32; 4], String>;
35}
36
37#[derive(Clone, Debug)]
39pub enum View {
40 Uniform([f32; 4]),
42 Constant([f32; 4]),
45 Texture2d(Decoded),
47 Texture3d(Decoded),
49 Image(Arc<dyn Image>),
51 Typed(Arc<[[u32; 4]]>),
53 Structured {
55 stride: u32,
57 words: Vec<u32>,
59 },
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum ViewFilter {
65 Point,
67 Linear {
69 subtexel: Subtexel,
71 },
72}
73
74#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct ViewSampler {
77 pub filter: ViewFilter,
79 pub address: [Address; 3],
81 pub border: Option<[f32; 4]>,
84}
85
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub enum Absent {
90 #[default]
93 Zero,
94 Refuse,
96}
97
98#[derive(Clone, Debug, Default)]
101pub struct Views {
102 pub views: BTreeMap<u32, View>,
104 pub samplers: BTreeMap<u32, ViewSampler>,
106 pub absent: Absent,
108}
109
110impl Views {
111 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 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 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 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 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 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 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 #[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 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 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 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 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 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 #[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 #[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}