use iridia_interface::{AddressMode, SamplerState};
use primase_foundations::{
sampling::Address, sampling::Decoded, sampling::Levels, sampling::Subtexel, sampling::Transfer,
sampling::footprint_lod,
};
use crate::{Image, Lod};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Filtering {
BaseBilinear {
subtexel: Subtexel,
},
Point,
Bilinear,
Trilinear,
}
impl Filtering {
fn level_of_detail(
lod: Lod,
size: [u32; 2],
sampler_bias: f64,
footprintless: Option<f32>,
) -> Result<f64, String> {
let [w, h] = size;
let grad = |g: [[f32; 2]; 2]| g.map(|d| d.map(f64::from));
let program = |b: f32| f64::from(b.clamp(-16.0, 15.99));
let without = || {
footprintless
.map(f64::from)
.ok_or_else(|| "an implicit level without a quad's footprint".to_owned())
};
Ok(match lod {
Lod::Gradients(g) => footprint_lod(w, h, grad(g), sampler_bias),
Lod::GradientsBias(g, b) => footprint_lod(w, h, grad(g), program(b) + sampler_bias),
Lod::Level(l) => f64::from(l) + sampler_bias,
Lod::Implicit => without()? + sampler_bias,
Lod::Bias(b) => without()? + program(b) + sampler_bias,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unstated {
Refuse,
Assume(Address),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SamplerUse {
pub address: [Address; 2],
pub mip_lod_bias: Option<f32>,
}
impl SamplerUse {
pub fn addressed(address: [Address; 2]) -> SamplerUse {
SamplerUse {
address,
mip_lod_bias: None,
}
}
pub fn of(state: &SamplerState, unstated: Unstated) -> Result<SamplerUse, String> {
let axis = |k: usize| -> Result<Address, String> {
let name = ["u", "v"].get(k).copied().unwrap_or("w");
Ok(match state.wrap.get(k).copied().flatten() {
Some(AddressMode::Repeat) => Address::Wrap,
Some(AddressMode::Mirror) => Address::Mirror,
Some(AddressMode::Clamp) => Address::Clamp,
Some(AddressMode::MirrorOnce) => Address::MirrorOnce,
Some(AddressMode::Border) => {
return Err(format!(
"border addressing along {name}: the source stores no border colour"
));
}
None => match unstated {
Unstated::Assume(a) => a,
Unstated::Refuse => {
return Err(format!("the sampler states no addressing along {name}"));
}
},
})
};
if let Some(b) = state.mip_lod_bias
&& !b.is_finite()
{
return Err(format!("sampler bias {b} is not finite"));
}
Ok(SamplerUse {
address: [axis(0)?, axis(1)?],
mip_lod_bias: state.mip_lod_bias,
})
}
pub fn assumed(state: &SamplerState) -> bool {
state.wrap[0].is_none() || state.wrap[1].is_none()
}
}
#[derive(Clone, Debug)]
pub struct SampledTexture {
levels: Levels,
transfer: Transfer,
sampler: SamplerUse,
filtering: Filtering,
footprintless: Option<f32>,
base: Option<Decoded>,
}
impl SampledTexture {
pub fn new(
levels: Levels,
transfer: Transfer,
sampler: SamplerUse,
filtering: Filtering,
) -> Result<SampledTexture, String> {
if levels.levels.is_empty() {
return Err("a texture with no stored level".into());
}
if let Some(b) = sampler.mip_lod_bias
&& !b.is_finite()
{
return Err(format!("sampler bias {b} is not finite"));
}
let base = match filtering {
Filtering::BaseBilinear { .. } => {
Some(levels.decoded(0, transfer).map_err(|e| format!("level 0: {e}"))?)
}
Filtering::Point | Filtering::Bilinear | Filtering::Trilinear => None,
};
Ok(SampledTexture {
levels,
transfer,
sampler,
filtering,
footprintless: None,
base,
})
}
pub fn with_footprintless_level(mut self, level: f32) -> Result<SampledTexture, String> {
if !level.is_finite() {
return Err(format!("footprintless level {level} is not finite"));
}
self.footprintless = Some(level);
Ok(self)
}
pub fn footprintless_level(&self) -> Option<f32> {
self.footprintless
}
pub fn levels(&self) -> &Levels {
&self.levels
}
pub fn transfer(&self) -> Transfer {
self.transfer
}
pub fn sampler(&self) -> SamplerUse {
self.sampler
}
pub fn address(&self) -> [Address; 2] {
self.sampler.address
}
pub fn filtering(&self) -> Filtering {
self.filtering
}
pub fn dimensions(&self) -> [usize; 2] {
[self.levels.width as usize, self.levels.height as usize]
}
pub fn decoded_bytes(&self) -> usize {
self.levels
.levels
.iter()
.map(Vec::len)
.fold(0usize, usize::saturating_add)
.saturating_add(self.base.as_ref().map_or(0, Decoded::decoded_bytes))
}
pub fn unhonoured(&self) -> Option<&'static str> {
match (self.filtering, self.sampler.mip_lod_bias) {
(Filtering::BaseBilinear { .. }, Some(b)) if b != 0.0 => {
Some("stated sampler bias not applied (base level)")
}
_ => None,
}
}
}
impl Image for SampledTexture {
fn sample(&self, uv: [f32; 2], lod: Lod) -> Result<[f32; 4], String> {
let address = self.sampler.address;
let level = || {
Filtering::level_of_detail(
lod,
[self.levels.width, self.levels.height],
self.sampler.mip_lod_bias.map_or(0.0, f64::from),
self.footprintless,
)
};
let (at, levels, transfer) = ([f64::from(uv[0]), f64::from(uv[1])], &self.levels, self.transfer);
let v = match (self.filtering, &self.base) {
(Filtering::BaseBilinear { subtexel }, Some(base)) => {
return base.bilinear_at(uv, address, subtexel).map_err(|e| e.to_string());
}
(Filtering::BaseBilinear { .. }, None) => {
return Err("a base-level texture whose base was not decoded when it was made".into());
}
(Filtering::Point, _) => levels
.nearest_level(level()?)
.and_then(|l| levels.point(l, at, address, transfer)),
(Filtering::Bilinear, _) => levels
.nearest_level(level()?)
.and_then(|l| levels.bilinear(l, at, address, transfer)),
(Filtering::Trilinear, _) => levels.trilinear(at, level()?, address, transfer),
}
.map_err(|e| e.to_string())?;
Ok(v.map(crate::narrow))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn state(u: Option<AddressMode>, v: Option<AddressMode>, bias: Option<f32>) -> SamplerState {
SamplerState {
wrap: [u, v, None],
mip_lod_bias: bias,
..SamplerState::default()
}
}
fn two_texels() -> Levels {
Levels::one(2, 1, vec![0, 0, 0, 255, 255, 255, 255, 255]).unwrap()
}
#[test]
fn the_sampler_states_the_addressing() {
let clamp = SamplerUse::of(
&state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), None),
Unstated::Refuse,
)
.unwrap();
assert_eq!(clamp.address, [Address::Clamp; 2]);
let base = Filtering::BaseBilinear {
subtexel: Subtexel::D3d11,
};
let clamped = SampledTexture::new(two_texels(), Transfer::Stored, clamp, base).unwrap();
let wrapped = SampledTexture::new(
two_texels(),
Transfer::Stored,
SamplerUse::addressed([Address::Wrap; 2]),
base,
)
.unwrap();
assert_eq!(clamped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.0);
assert_eq!(wrapped.sample([0.0, 0.5], Lod::Implicit).unwrap()[0], 0.5);
assert!(SamplerUse::of(&state(None, Some(AddressMode::Clamp), None), Unstated::Refuse).is_err());
let assumed = SamplerUse::of(
&state(None, Some(AddressMode::Clamp), None),
Unstated::Assume(Address::Wrap),
)
.unwrap();
assert_eq!(assumed.address, [Address::Wrap, Address::Clamp]);
assert!(SamplerUse::assumed(&state(None, Some(AddressMode::Clamp), None)));
assert!(
SamplerUse::of(
&state(Some(AddressMode::Border), Some(AddressMode::Clamp), None),
Unstated::Refuse
)
.is_err()
);
assert!(
SamplerUse::of(
&state(Some(AddressMode::Clamp), Some(AddressMode::Clamp), Some(f32::NAN)),
Unstated::Refuse
)
.is_err()
);
}
#[test]
fn a_stated_bias_is_applied_or_named() {
let levels = Levels {
width: 2,
height: 2,
levels: vec![[0, 0, 0, 255].repeat(4), vec![255; 4]],
};
let make = |bias: Option<f32>, filtering| {
SampledTexture::new(
levels.clone(),
Transfer::Stored,
SamplerUse {
address: [Address::Wrap; 2],
mip_lod_bias: bias,
},
filtering,
)
.unwrap()
};
let footprint = Lod::Gradients([[0.25, 0.0], [0.0, 0.25]]);
let plain = make(None, Filtering::Trilinear);
assert_eq!(plain.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
let biased = make(Some(2.0), Filtering::Trilinear);
assert_eq!(biased.sample([0.5, 0.5], footprint).unwrap()[0], 1.0);
assert_eq!(biased.unhonoured(), None);
assert_eq!(biased.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 1.0);
assert_eq!(plain.sample([0.5, 0.5], Lod::Level(-1.0)).unwrap()[0], 0.0);
assert_eq!(plain.sample([0.5, 0.5], Lod::Level(1.0)).unwrap()[0], 1.0);
assert!(plain.sample([0.5, 0.5], Lod::Implicit).is_err());
let base = make(
Some(2.0),
Filtering::BaseBilinear {
subtexel: Subtexel::D3d11,
},
);
assert_eq!(base.sample([0.5, 0.5], footprint).unwrap()[0], 0.0);
assert_eq!(
base.unhonoured(),
Some("stated sampler bias not applied (base level)")
);
}
#[test]
fn nearest_level_filters_and_a_stated_footprintless_level() {
let levels = Levels {
width: 2,
height: 2,
levels: vec![[[0, 0, 0, 255], [255; 4]].concat().repeat(2), vec![128; 4]],
};
let make = |filtering| {
SampledTexture::new(
levels.clone(),
Transfer::Stored,
SamplerUse::addressed([Address::Wrap; 2]),
filtering,
)
.unwrap()
};
let grey = 128.0 / 255.0;
let point = make(Filtering::Point);
assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.0)).unwrap()[0], 0.0);
assert_eq!(point.sample([0.75, 0.25], Lod::Level(0.49)).unwrap()[0], 1.0);
assert_eq!(point.sample([0.25, 0.25], Lod::Level(0.5)).unwrap()[0], grey);
let bilinear = make(Filtering::Bilinear);
assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.4)).unwrap()[0], 0.5);
assert_eq!(bilinear.sample([0.5, 0.5], Lod::Level(0.6)).unwrap()[0], grey);
assert!(point.sample([0.25, 0.25], Lod::Implicit).is_err());
assert!(point.sample([0.25, 0.25], Lod::Bias(0.0)).is_err());
let stated = make(Filtering::Point).with_footprintless_level(1.0).unwrap();
assert_eq!(stated.footprintless_level(), Some(1.0));
assert_eq!(stated.sample([0.25, 0.25], Lod::Implicit).unwrap()[0], grey);
assert_eq!(stated.sample([0.25, 0.25], Lod::Bias(-1.0)).unwrap()[0], 0.0);
assert!(make(Filtering::Point).with_footprintless_level(f32::NAN).is_err());
}
}