Skip to main content

iridia_interface/
state.rs

1//! Fixed-function state as Direct3D states it: the sampler state a texture is read through and the blend, depth,
2//! stencil, cull, fill and alpha test a program is drawn with.
3
4use serde::Serialize;
5
6/// How texels are filtered when a texture is magnified or minified (glTF's sampler filters).
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
8pub enum Filter {
9    /// The nearest texel: crisp texels.
10    Nearest,
11    /// Bilinear between the four nearest texels.
12    Linear,
13}
14/// How mip levels are used when a texture is minified.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
16pub enum MipFilter {
17    /// No mip levels: the base level only (glTF's `NEAREST` or `LINEAR` minification filter).
18    None,
19    /// The nearest mip level.
20    Nearest,
21    /// Between the two nearest mip levels (trilinear with a linear `min`).
22    Linear,
23}
24/// A sampler's filters as the source states them: each `None` where it states none (the renderer's default).
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
26pub struct Filters {
27    /// Magnification.
28    pub mag: Option<Filter>,
29    /// Minification within a level.
30    pub min: Option<Filter>,
31    /// Minification across mip levels.
32    pub mip: Option<MipFilter>,
33}
34
35/// What a texture coordinate outside 0 to 1 reads, along one axis.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
37#[serde(rename_all = "snake_case")]
38pub enum AddressMode {
39    /// The texture repeats.
40    Repeat,
41    /// The texture repeats, every other copy mirrored.
42    Mirror,
43    /// The edge texel.
44    Clamp,
45    /// The sampler's border colour.
46    Border,
47    /// Mirrored once about the origin, then the edge texel.
48    MirrorOnce,
49}
50/// A sampler's state as the source stores it, for a texture slot or a program's sampler: each part `None` where the
51/// source states none.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
53pub struct SamplerState {
54    /// Addressing along u, v and w.
55    pub wrap: [Option<AddressMode>; 3],
56    /// The sampler's filters.
57    pub filters: Filters,
58    /// The most texels along the axis of anisotropy an anisotropic filter takes (1: none beyond the filters'), where
59    /// the source states an anisotropic filter.
60    pub max_anisotropy: Option<u8>,
61    /// The comparison a depth-comparison sampler makes of the reference against the texel, where the sampler is one.
62    pub compare: Option<Compare>,
63    /// Added to the mip level the hardware computes, where the source states a non-zero bias.
64    pub mip_lod_bias: Option<f32>,
65    /// The colour (red, green, blue, alpha) a coordinate outside the texture reads along an axis addressed
66    /// [`AddressMode::Border`], where the source states it.
67    pub border: Option<[f32; 4]>,
68    /// The least and the greatest mip level the sampler reads (the level of detail is clamped to them), where the
69    /// source states a range; `None` is every level the texture has.
70    pub lod: Option<[f32; 2]>,
71}
72
73impl SamplerState {
74    /// The state agrees with itself: a stated border colour and level range are finite (the range's top may be
75    /// `f32::MAX`, every level), the range is not reversed, and a maximum anisotropy is at least 1.
76    pub fn verify(&self) -> Result<(), String> {
77        if let Some(b) = self.border
78            && b.iter().any(|c| !c.is_finite())
79        {
80            return Err(format!("border colour {b:?} is not finite"));
81        }
82        if let Some([low, high]) = self.lod
83            && (!low.is_finite() || !high.is_finite() || low > high)
84        {
85            return Err(format!(
86                "level range {low}..{high} is not a finite, ordered range"
87            ));
88        }
89        if let Some(b) = self.mip_lod_bias
90            && !b.is_finite()
91        {
92            return Err(format!("mip level bias {b} is not finite"));
93        }
94        if self.max_anisotropy == Some(0) {
95            return Err("a maximum anisotropy of 0".into());
96        }
97        Ok(())
98    }
99}
100/// A blend factor.
101#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
102#[serde(rename_all = "snake_case")]
103pub enum BlendFactor {
104    /// Zero.
105    Zero,
106    /// One.
107    One,
108    /// The source colour.
109    SrcColour,
110    /// One minus the source colour.
111    OneMinusSrcColour,
112    /// The source alpha.
113    SrcAlpha,
114    /// One minus the source alpha.
115    OneMinusSrcAlpha,
116    /// The colour already in the target.
117    DstColour,
118    /// One minus the target colour.
119    OneMinusDstColour,
120    /// The alpha already in the target.
121    DstAlpha,
122    /// One minus the target alpha.
123    OneMinusDstAlpha,
124}
125/// How the two weighted terms of a blend are combined.
126#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
127#[serde(rename_all = "snake_case")]
128pub enum BlendOp {
129    /// `source + destination`.
130    Add,
131    /// `source - destination`.
132    Subtract,
133    /// `destination - source`.
134    ReverseSubtract,
135    /// The smaller of the two unweighted values.
136    Min,
137    /// The larger of the two unweighted values.
138    Max,
139}
140/// `op(src * source, dst * destination)`.
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
142pub struct BlendEquation {
143    /// The factor the incoming (source) value is multiplied by.
144    pub source: BlendFactor,
145    /// The factor the value already in the target is multiplied by.
146    pub destination: BlendFactor,
147    /// How the two products are combined.
148    pub op: BlendOp,
149}
150/// Blending of the colour and the alpha channel.
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
152pub struct Blend {
153    /// The equation for the red, green and blue channels.
154    pub colour: BlendEquation,
155    /// The equation for the alpha channel.
156    pub alpha: BlendEquation,
157}
158/// How two values are compared; the left one is the fragment's own (its depth or alpha, or the stencil reference), the
159/// right one what it is tested against.
160#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
161#[serde(rename_all = "snake_case")]
162pub enum Compare {
163    /// Never passes.
164    Never,
165    /// Passes where less.
166    Less,
167    /// Passes where equal.
168    Equal,
169    /// Passes where less than or equal.
170    LessEqual,
171    /// Passes where greater.
172    Greater,
173    /// Passes where not equal.
174    NotEqual,
175    /// Passes where greater than or equal.
176    GreaterEqual,
177    /// Always passes.
178    Always,
179}
180/// Which facing of triangle is not drawn.
181#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
182#[serde(rename_all = "snake_case")]
183pub enum Cull {
184    /// Neither: both facings are drawn.
185    None,
186    /// Front-facing triangles are culled.
187    Front,
188    /// Back-facing triangles are culled.
189    Back,
190}
191/// Depth test and write.
192#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
193pub struct DepthState {
194    /// The depth comparison a fragment must pass.
195    pub test: Compare,
196    /// Whether a passing fragment writes its depth.
197    pub write: bool,
198}
199/// Alpha test: a pixel is kept where `alpha compare reference`.
200#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
201pub struct AlphaTest {
202    /// The comparison of the fragment's alpha with `reference`.
203    pub compare: Compare,
204    /// The reference alpha, 0 to 1.
205    pub reference: f32,
206}
207/// A stencil operation (D3D12 and Vulkan have the same eight).
208#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
209#[serde(rename_all = "snake_case")]
210pub enum StencilOp {
211    /// Keep the stored value.
212    Keep,
213    /// Set it to zero.
214    Zero,
215    /// Set it to the reference value.
216    Replace,
217    /// Increment, clamped at the maximum.
218    IncrementClamp,
219    /// Decrement, clamped at zero.
220    DecrementClamp,
221    /// Invert its bits.
222    Invert,
223    /// Increment, wrapping to zero.
224    IncrementWrap,
225    /// Decrement, wrapping to the maximum.
226    DecrementWrap,
227}
228/// What the stencil test does for one facing.
229#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
230pub struct StencilFace {
231    /// On a failed stencil test.
232    pub fail: StencilOp,
233    /// On a passed stencil test and a failed depth test.
234    pub depth_fail: StencilOp,
235    /// On both passed.
236    pub pass: StencilOp,
237    /// The comparison of the reference with the stored value.
238    pub compare: Compare,
239}
240/// An enabled stencil test (a disabled one is `None` in [`RenderState::stencil`]).
241#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
242pub struct Stencil {
243    /// Mask applied to the reference and the stored value before they are compared.
244    pub read_mask: u8,
245    /// Mask of the stencil bits a write changes.
246    pub write_mask: u8,
247    /// The reference value the test compares against and `Replace` writes, where the source states one (D3D12 sets it
248    /// per draw: a source that stores it per draw states none).
249    pub reference: Option<u8>,
250    /// The operations for front-facing triangles.
251    pub front: StencilFace,
252    /// The operations for back-facing triangles.
253    pub back: StencilFace,
254}
255/// What one render target does with a fragment: its blend and which channels it writes.
256#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
257pub struct TargetState {
258    /// `None` where the target is not blended.
259    pub blend: Option<Blend>,
260    /// The channels written: bit 0 red, 1 green, 2 blue, 3 alpha (`0xf` all).
261    pub write_mask: u8,
262}
263/// A depth bias, D3D12's three terms: `bias = constant * r + slope_scaled * max_slope`, limited to `clamp` when that
264/// is not zero (r the smallest resolvable depth difference).
265#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
266pub struct DepthBias {
267    /// The constant term, in units of the smallest resolvable depth difference.
268    pub constant: f32,
269    /// The factor of the polygon's maximum depth slope.
270    pub slope_scaled: f32,
271    /// The most the bias may reach in magnitude; 0 for no limit.
272    pub clamp: f32,
273}
274/// How a triangle is filled.
275#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
276#[serde(rename_all = "snake_case")]
277pub enum Fill {
278    /// Filled triangles.
279    Solid,
280    /// Triangle outlines.
281    Wireframe,
282}
283/// Fixed-function state as the source stores it; each `None` where it states none. D3D12-shaped: `blend` is render
284/// target 0's blend (what a consumer with one render target reads), and `targets` has every render target's blend
285/// and write mask where the source states them, with `targets[0]`'s blend equal to `blend` (a consumer's
286/// verification checks it).
287#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
288pub struct RenderState {
289    /// Blending of render target 0; `None` where it is not blended.
290    pub blend: Option<Blend>,
291    /// The depth test and write.
292    pub depth: Option<DepthState>,
293    /// The face culling.
294    pub cull: Option<Cull>,
295    /// The alpha test.
296    pub alpha_test: Option<AlphaTest>,
297    /// Per render target, in order.
298    pub targets: [Option<TargetState>; 8],
299    /// The stencil test, where one is enabled.
300    pub stencil: Option<Stencil>,
301    /// The depth bias.
302    pub depth_bias: Option<DepthBias>,
303    /// How triangles are filled.
304    pub fill: Option<Fill>,
305    /// Whether counter-clockwise triangles are front faces.
306    pub front_counter_clockwise: Option<bool>,
307}
308impl RenderState {
309    /// The state is consistent: the write masks fit four channels, the bias is finite, and `blend` is the first
310    /// target's blend (a state with a blend has a first target, and the other way round).
311    pub fn verify(&self) -> Result<(), String> {
312        if let Some(a) = self.alpha_test
313            && !a.reference.is_finite()
314        {
315            return Err("the alpha test's reference is not finite".into());
316        }
317        for (i, t) in self.targets.iter().enumerate() {
318            if let Some(t) = t
319                && t.write_mask > 0xf
320            {
321                return Err(format!(
322                    "render target {i}'s write mask {:#x} is wider than four channels",
323                    t.write_mask
324                ));
325            }
326        }
327        match self.targets[0] {
328            Some(first) if first.blend != self.blend => {
329                return Err("render target 0's blend is not the state's blend".into());
330            }
331            None if self.blend.is_some() => {
332                return Err("the state has a blend and render target 0 has no state to hold it".into());
333            }
334            _ => {}
335        }
336        if let Some(b) = self.depth_bias
337            && !(b.constant.is_finite() && b.slope_scaled.is_finite() && b.clamp.is_finite())
338        {
339            return Err("the depth bias is not finite".into());
340        }
341        Ok(())
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn a_render_state_is_consistent() {
351        let eq = BlendEquation {
352            source: BlendFactor::SrcAlpha,
353            destination: BlendFactor::OneMinusSrcAlpha,
354            op: BlendOp::Add,
355        };
356        let blend = Blend {
357            colour: eq,
358            alpha: eq,
359        };
360        let mut state = RenderState::default();
361        assert_eq!(state.verify(), Ok(()));
362        // the first target's blend is the state's
363        state.targets[0] = Some(TargetState {
364            blend: Some(blend),
365            write_mask: 0xf,
366        });
367        assert!(state.verify().unwrap_err().contains("target 0's blend"));
368        state.blend = Some(blend);
369        assert_eq!(state.verify(), Ok(()));
370        // a blend with no state for the first target to hold it is not the same as no blend
371        let mut lone = RenderState {
372            blend: Some(blend),
373            ..RenderState::default()
374        };
375        assert!(lone.verify().unwrap_err().contains("no state to hold it"));
376        lone.targets[0] = Some(TargetState {
377            blend: Some(blend),
378            write_mask: 0xf,
379        });
380        assert_eq!(lone.verify(), Ok(()));
381        // other targets are free; a mask is four channels
382        state.targets[3] = Some(TargetState {
383            blend: None,
384            write_mask: 0x1f,
385        });
386        assert!(state.verify().unwrap_err().contains("target 3's write mask"));
387        state.targets[3] = Some(TargetState {
388            blend: None,
389            write_mask: 0x8,
390        });
391        state.depth_bias = Some(DepthBias {
392            constant: 0.0,
393            slope_scaled: f32::NAN,
394            clamp: 0.0,
395        });
396        assert!(state.verify().unwrap_err().contains("depth bias"));
397    }
398
399    #[test]
400    fn a_sampler_state_is_consistent() {
401        let mut state = SamplerState {
402            wrap: [Some(AddressMode::Border); 3],
403            border: Some([0.0, 0.0, 0.0, 0.0]),
404            lod: Some([0.0, 0.0]),
405            ..SamplerState::default()
406        };
407        assert_eq!(state.verify(), Ok(()));
408        assert_eq!(SamplerState::default().verify(), Ok(()));
409        // Direct3D's greatest level, f32::MAX, is every level and finite
410        state.lod = Some([0.0, f32::MAX]);
411        assert_eq!(state.verify(), Ok(()));
412        state.lod = Some([2.0, 1.0]);
413        assert!(state.verify().unwrap_err().contains("level range"));
414        state.lod = Some([0.0, f32::INFINITY]);
415        assert!(state.verify().unwrap_err().contains("level range"));
416        state.lod = None;
417        state.border = Some([0.0, f32::NAN, 0.0, 1.0]);
418        assert!(state.verify().unwrap_err().contains("border colour"));
419        state.border = Some([1.0; 4]);
420        state.mip_lod_bias = Some(f32::NAN);
421        assert!(state.verify().unwrap_err().contains("bias"));
422        state.mip_lod_bias = Some(-0.75);
423        state.max_anisotropy = Some(0);
424        assert!(state.verify().unwrap_err().contains("anisotropy"));
425        state.max_anisotropy = Some(16);
426        assert_eq!(state.verify(), Ok(()));
427    }
428}