Skip to main content

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