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}