1use iridia_interface::{AddressMode, SamplerState};
17use primase_foundations::{
18 sampling::Address, sampling::Addressing, sampling::Decoded, sampling::Levels, sampling::Subtexel,
19 sampling::Transfer, sampling::footprint_lod,
20};
21
22use crate::{Image, Lod};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Filtering {
27 BaseBilinear {
30 subtexel: Subtexel,
32 },
33 Point,
36 Bilinear,
39 Trilinear,
41}
42
43impl Filtering {
44 fn level_of_detail(
50 lod: Lod,
51 size: [u32; 2],
52 sampler_bias: f64,
53 footprintless: Option<f32>,
54 ) -> Result<f64, String> {
55 let [w, h] = size;
56 let grad = |g: [[f32; 2]; 2]| g.map(|d| d.map(f64::from));
57 let program = |b: f32| f64::from(b.clamp(-16.0, 15.99));
58 let without = || {
59 footprintless
60 .map(f64::from)
61 .ok_or_else(|| "an implicit level without a quad's footprint".to_owned())
62 };
63 Ok(match lod {
64 Lod::Gradients(g) => footprint_lod(w, h, grad(g), sampler_bias),
65 Lod::GradientsBias(g, b) => footprint_lod(w, h, grad(g), program(b) + sampler_bias),
66 Lod::Level(l) => f64::from(l) + sampler_bias,
67 Lod::Implicit => without()? + sampler_bias,
68 Lod::Bias(b) => without()? + program(b) + sampler_bias,
69 })
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum Unstated {
76 Refuse,
78 Assume(Address),
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
85pub struct SamplerUse {
86 pub address: [Address; 2],
88 pub border: Option<[f32; 4]>,
91 pub mip_lod_bias: Option<f32>,
93 pub lod: Option<[f32; 2]>,
96}
97
98impl SamplerUse {
99 pub fn addressed(address: [Address; 2]) -> SamplerUse {
101 SamplerUse {
102 address,
103 border: None,
104 mip_lod_bias: None,
105 lod: None,
106 }
107 }
108
109 pub fn of(state: &SamplerState, unstated: Unstated) -> Result<SamplerUse, String> {
114 state.verify()?;
115 let axis = |k: usize| -> Result<Address, String> {
116 let name = ["u", "v"].get(k).copied().unwrap_or("w");
117 Ok(match state.wrap.get(k).copied().flatten() {
118 Some(AddressMode::Repeat) => Address::Wrap,
119 Some(AddressMode::Mirror) => Address::Mirror,
120 Some(AddressMode::Clamp) => Address::Clamp,
121 Some(AddressMode::MirrorOnce) => Address::MirrorOnce,
122 Some(AddressMode::Border) if state.border.is_some() => Address::Border,
123 Some(AddressMode::Border) => {
124 return Err(format!(
125 "border addressing along {name}: the sampler states no border colour"
126 ));
127 }
128 None => match unstated {
129 Unstated::Assume(a) => a,
130 Unstated::Refuse => {
131 return Err(format!("the sampler states no addressing along {name}"));
132 }
133 },
134 })
135 };
136 Ok(SamplerUse {
137 address: [axis(0)?, axis(1)?],
138 border: state.border,
139 mip_lod_bias: state.mip_lod_bias,
140 lod: state.lod,
141 })
142 }
143
144 fn addressing(&self) -> Addressing<2> {
146 Addressing {
147 axes: self.address,
148 border: self.border,
149 }
150 }
151
152 fn clamped(&self, level: f64) -> f64 {
154 match self.lod {
155 Some([low, high]) => level.clamp(f64::from(low), f64::from(high)),
156 None => level,
157 }
158 }
159
160 pub fn assumed(state: &SamplerState) -> bool {
162 state.wrap[0].is_none() || state.wrap[1].is_none()
163 }
164}
165
166#[derive(Clone, Debug)]
168pub struct SampledTexture {
169 levels: Levels,
170 transfer: Transfer,
171 sampler: SamplerUse,
172 filtering: Filtering,
173 footprintless: Option<f32>,
175 base: Option<Decoded>,
177}
178
179impl SampledTexture {
180 pub fn new(
183 levels: Levels,
184 transfer: Transfer,
185 sampler: SamplerUse,
186 filtering: Filtering,
187 ) -> Result<SampledTexture, String> {
188 if levels.levels.is_empty() {
189 return Err("a texture with no stored level".into());
190 }
191 if let Some(b) = sampler.mip_lod_bias
192 && !b.is_finite()
193 {
194 return Err(format!("sampler bias {b} is not finite"));
195 }
196 if let Some([low, high]) = sampler.lod
197 && (!low.is_finite() || !high.is_finite() || low > high)
198 {
199 return Err(format!(
200 "sampler level range {low}..{high} is not a finite, ordered range"
201 ));
202 }
203 match sampler.border {
204 Some(c) if c.iter().any(|v| !v.is_finite()) => {
205 return Err(format!("border colour {c:?} is not finite"));
206 }
207 None if sampler.address.contains(&Address::Border) => {
208 return Err("border addressing with no border colour".into());
209 }
210 _ => {}
211 }
212 let base = match filtering {
213 Filtering::BaseBilinear { .. } => {
214 Some(levels.decoded(0, transfer).map_err(|e| format!("level 0: {e}"))?)
215 }
216 Filtering::Point | Filtering::Bilinear | Filtering::Trilinear => None,
217 };
218 Ok(SampledTexture {
219 levels,
220 transfer,
221 sampler,
222 filtering,
223 footprintless: None,
224 base,
225 })
226 }
227
228 pub fn with_footprintless_level(mut self, level: f32) -> Result<SampledTexture, String> {
232 if !level.is_finite() {
233 return Err(format!("footprintless level {level} is not finite"));
234 }
235 self.footprintless = Some(level);
236 Ok(self)
237 }
238
239 pub fn footprintless_level(&self) -> Option<f32> {
241 self.footprintless
242 }
243
244 pub fn levels(&self) -> &Levels {
246 &self.levels
247 }
248
249 pub fn transfer(&self) -> Transfer {
251 self.transfer
252 }
253
254 pub fn sampler(&self) -> SamplerUse {
256 self.sampler
257 }
258
259 pub fn address(&self) -> [Address; 2] {
261 self.sampler.address
262 }
263
264 pub fn filtering(&self) -> Filtering {
266 self.filtering
267 }
268
269 pub fn dimensions(&self) -> [usize; 2] {
271 [self.levels.width as usize, self.levels.height as usize]
272 }
273
274 pub fn decoded_bytes(&self) -> usize {
276 self.levels
277 .levels
278 .iter()
279 .map(Vec::len)
280 .fold(0usize, usize::saturating_add)
281 .saturating_add(self.base.as_ref().map_or(0, Decoded::decoded_bytes))
282 }
283
284 pub fn unhonoured(&self) -> Option<&'static str> {
288 if !matches!(self.filtering, Filtering::BaseBilinear { .. }) {
289 return None;
290 }
291 let bias = self.sampler.mip_lod_bias.is_some_and(|b| b != 0.0);
292 let range = self.sampler.lod.is_some_and(|[low, _]| low > 0.0);
293 match (bias, range) {
294 (true, true) => Some("stated sampler bias and level range not applied (base level)"),
295 (true, false) => Some("stated sampler bias not applied (base level)"),
296 (false, true) => Some("stated sampler level range not applied (base level)"),
297 (false, false) => None,
298 }
299 }
300}
301
302impl Image for SampledTexture {
303 fn sample(&self, uv: [f32; 2], lod: Lod) -> Result<[f32; 4], String> {
304 let address = self.sampler.addressing();
305 let level = || {
306 Filtering::level_of_detail(
307 lod,
308 [self.levels.width, self.levels.height],
309 self.sampler.mip_lod_bias.map_or(0.0, f64::from),
310 self.footprintless,
311 )
312 .map(|l| self.sampler.clamped(l))
313 };
314 let (at, levels, transfer) = ([f64::from(uv[0]), f64::from(uv[1])], &self.levels, self.transfer);
315 let v = match (self.filtering, &self.base) {
316 (Filtering::BaseBilinear { subtexel }, Some(base)) => {
317 return base.bilinear_at(uv, address, subtexel).map_err(|e| e.to_string());
318 }
319 (Filtering::BaseBilinear { .. }, None) => {
320 return Err("a base-level texture whose base was not decoded when it was made".into());
321 }
322 (Filtering::Point, _) => levels
323 .nearest_level(level()?)
324 .and_then(|l| levels.point(l, at, address, transfer)),
325 (Filtering::Bilinear, _) => levels
326 .nearest_level(level()?)
327 .and_then(|l| levels.bilinear(l, at, address, transfer)),
328 (Filtering::Trilinear, _) => levels.trilinear(at, level()?, address, transfer),
329 }
330 .map_err(|e| e.to_string())?;
331 Ok(v.map(crate::narrow))
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn state(u: Option<AddressMode>, v: Option<AddressMode>, bias: Option<f32>) -> SamplerState {
340 SamplerState {
341 wrap: [u, v, None],
342 mip_lod_bias: bias,
343 ..SamplerState::default()
344 }
345 }
346
347 fn two_texels() -> Levels {
349 Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255]).unwrap()
350 }
351
352 #[test]
355 fn the_sampler_states_the_addressing() {
356 let clamp = SamplerUse::of(
357 &state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), None),
358 Unstated::Refuse,
359 )
360 .unwrap();
361 assert_eq!(clamp.address, [Address::Clamp; 2]);
362 let base = Filtering::BaseBilinear {
363 subtexel: Subtexel::D3d11,
364 };
365 let clamped = SampledTexture::new(two_texels(), Transfer::Stored, clamp, base).unwrap();
366 let wrapped = SampledTexture::new(
367 two_texels(),
368 Transfer::Stored,
369 SamplerUse::addressed([Address::Wrap; 2]),
370 base,
371 )
372 .unwrap();
373 assert_eq!(clamped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.0);
375 assert_eq!(wrapped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.5);
376 assert!(SamplerUse::of(&state(None, Some(AddressMode::Clamp), None), Unstated::Refuse).is_err());
377 let assumed = SamplerUse::of(
378 &state(None, Some(AddressMode::Clamp), None),
379 Unstated::Assume(Address::Wrap),
380 )
381 .unwrap();
382 assert_eq!(assumed.address, [Address::Wrap, Address::Clamp]);
383 assert!(SamplerUse::assumed(&state(None, Some(AddressMode::Clamp), None)));
384 assert!(
386 SamplerUse::of(
387 &state(Some(AddressMode::Border), Some(AddressMode::Clamp), None),
388 Unstated::Refuse
389 )
390 .is_err()
391 );
392 assert!(
393 SamplerUse::of(
394 &state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), Some(f32::NAN)),
395 Unstated::Refuse
396 )
397 .is_err()
398 );
399 }
400
401 #[test]
405 fn a_stated_border_colour_is_read_past_the_edge() {
406 let bordered = |colour: [f32; 4]| {
407 let s = SamplerState {
408 border: Some(colour),
409 ..state(Some(AddressMode::Border), Some(AddressMode::Clamp), None)
410 };
411 let sampler = SamplerUse::of(&s, Unstated::Refuse).unwrap();
412 assert_eq!(sampler.address, [Address::Border, Address::Clamp]);
413 sampler
414 };
415 for filtering in [
416 Filtering::BaseBilinear {
417 subtexel: Subtexel::D3d11,
418 },
419 Filtering::Bilinear,
420 ] {
421 let zero =
422 SampledTexture::new(two_texels(), Transfer::Stored, bordered([0.0; 4]), filtering).unwrap();
423 let one =
424 SampledTexture::new(two_texels(), Transfer::Stored, bordered([1.0; 4]), filtering).unwrap();
425 let past = |t: &SampledTexture| t.sample([-0.25, 0.5], Lod::Level(0.0)).unwrap();
427 assert_eq!(past(&zero), [0.0; 4]);
428 assert_eq!(past(&one), [1.0; 4]);
429 let edge = |t: &SampledTexture| t.sample([0.0, 0.5], Lod::Level(0.0)).unwrap();
431 assert_eq!(edge(&zero), [0.0, 0.0, 0.0, 0.5]);
432 assert_eq!(edge(&one), [0.5, 0.5, 0.5, 1.0]);
433 }
434 let nan = SamplerState {
435 border: Some([f32::NAN; 4]),
436 ..state(Some(AddressMode::Border), Some(AddressMode::Clamp), None)
437 };
438 assert!(SamplerUse::of(&nan, Unstated::Refuse).is_err());
439 let mut colourless = SamplerUse::addressed([Address::Border; 2]);
440 assert!(
441 SampledTexture::new(two_texels(), Transfer::Stored, colourless, Filtering::Bilinear).is_err()
442 );
443 colourless.border = Some([0.0; 4]);
444 assert!(SampledTexture::new(two_texels(), Transfer::Stored, colourless, Filtering::Bilinear).is_ok());
445 }
446
447 #[test]
451 fn a_stated_level_range_clamps_the_level() {
452 let levels = Levels {
453 width: 2,
454 height: 2,
455 levels: vec![[0, 0, 0, 255].repeat(4), vec![255; 4]],
456 };
457 let make = |lod: Option<[f32; 2]>, filtering| {
458 SampledTexture::new(
459 levels.clone(),
460 Transfer::Stored,
461 SamplerUse {
462 lod,
463 ..SamplerUse::addressed([Address::Wrap; 2])
464 },
465 filtering,
466 )
467 .unwrap()
468 };
469 let base_only = make(Some([0.0, 0.0]), Filtering::Trilinear);
470 assert_eq!(base_only.sample([0.5, 0.5], Lod::Level(1.0)).unwrap()[0], 0.0);
471 let first = make(Some([1.0, 1.0]), Filtering::Point);
472 assert_eq!(first.sample([0.5, 0.5], Lod::Level(0.0)).unwrap()[0], 1.0);
473 let unclamped = make(None, Filtering::Point);
474 assert_eq!(unclamped.sample([0.5, 0.5], Lod::Level(0.0)).unwrap()[0], 0.0);
475 let base = Filtering::BaseBilinear {
476 subtexel: Subtexel::D3d11,
477 };
478 assert_eq!(
479 make(Some([1.0, 1.0]), base).unhonoured(),
480 Some("stated sampler level range not applied (base level)")
481 );
482 assert_eq!(make(Some([0.0, 0.0]), base).unhonoured(), None);
483 let reversed = SamplerUse {
484 lod: Some([2.0, 1.0]),
485 ..SamplerUse::addressed([Address::Wrap; 2])
486 };
487 assert!(SampledTexture::new(levels.clone(), Transfer::Stored, reversed, Filtering::Point).is_err());
488 }
489
490 #[test]
493 fn a_stated_bias_is_applied_or_named() {
494 let levels = Levels {
496 width: 2,
497 height: 2,
498 levels: vec![[0, 0, 0, 255].repeat(4), vec![255; 4]],
499 };
500 let make = |bias: Option<f32>, filtering| {
501 SampledTexture::new(
502 levels.clone(),
503 Transfer::Stored,
504 SamplerUse {
505 mip_lod_bias: bias,
506 ..SamplerUse::addressed([Address::Wrap; 2])
507 },
508 filtering,
509 )
510 .unwrap()
511 };
512 let footprint = Lod::Gradients([[0.25, 0.0], [0.0, 0.25]]);
515 let plain = make(None, Filtering::Trilinear);
516 assert_eq!(plain.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
517 let biased = make(Some(2.0), Filtering::Trilinear);
518 assert_eq!(biased.sample([0.5, 0.5], footprint).unwrap()[0], 1.0);
519 assert_eq!(biased.unhonoured(), None);
520 assert_eq!(biased.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 1.0);
522 assert_eq!(plain.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 0.0);
523 assert_eq!(plain.sample([0.5, 0.5], Lod::Level(1.0)).unwrap()[0], 1.0);
524 assert!(plain.sample([0.5, 0.5], Lod::Implicit).is_err());
525 let base = make(
526 Some(2.0),
527 Filtering::BaseBilinear {
528 subtexel: Subtexel::D3d11,
529 },
530 );
531 assert_eq!(base.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
532 assert_eq!(
533 base.unhonoured(),
534 Some("stated sampler bias not applied (base level)")
535 );
536 }
537
538 #[test]
542 fn nearest_level_filters_and_a_stated_footprintless_level() {
543 let levels = Levels {
545 width: 2,
546 height: 2,
547 levels: vec![[[0, 0, 0, 255], [255; 4]].concat().repeat(2), vec![128; 4]],
548 };
549 let make = |filtering| {
550 SampledTexture::new(
551 levels.clone(),
552 Transfer::Stored,
553 SamplerUse::addressed([Address::Wrap; 2]),
554 filtering,
555 )
556 .unwrap()
557 };
558 let grey = 128.0 / 255.0;
559 let point = make(Filtering::Point);
560 assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.0)).unwrap()[0], 0.0);
561 assert_eq!(point.sample([0.75, 0.25], Lod::Level(0.49)).unwrap()[0], 1.0);
562 assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.5)).unwrap()[0], grey);
563 let bilinear = make(Filtering::Bilinear);
564 assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.4)).unwrap()[0], 0.5);
565 assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.6)).unwrap()[0], grey);
566 assert!(point.sample([0.25, 0.25], Lod::Implicit).is_err());
567 assert!(point.sample([0.25, 0.25], Lod::Bias(0.0)).is_err());
568 let stated = make(Filtering::Point).with_footprintless_level(1.0).unwrap();
569 assert_eq!(stated.footprintless_level(), Some(1.0));
570 assert_eq!(stated.sample([0.25, 0.25], Lod::Implicit).unwrap()[0], grey);
571 assert_eq!(stated.sample([0.25, 0.25], Lod::Bias(-1.0)).unwrap()[0], 0.0);
572 assert!(make(Filtering::Point).with_footprintless_level(f32::NAN).is_err());
573 }
574}