Skip to main content

primase_core/shader/
encoding.rs

1//! How a value is stored in the lanes of an output: the encodings of a normal ([`NormalEncoding`], in a
2//! [`NormalSpace`]) and of a scalar ([`ScalarEncoding`]), each with its decoding (and, for the packed normals, its
3//! encoding), as an output role states them (`OutputRole`).
4
5use serde::Serialize;
6
7/// Where a normal vector is expressed.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum NormalSpace {
11    /// World space.
12    World,
13    /// View (camera) space.
14    View,
15    /// Tangent space of the surface.
16    Tangent,
17}
18/// How a normal is stored in the lanes of an output.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
20#[serde(rename_all = "snake_case")]
21pub enum NormalEncoding {
22    /// x, y, z as signed values.
23    Xyz,
24    /// x, y, z mapped to 0-1 (`n * 0.5 + 0.5`).
25    XyzUnorm,
26    /// x and y signed, z from the unit length.
27    Xy,
28    /// x and y mapped to 0-1, z from the unit length.
29    XyUnorm,
30    /// x, y, z stored as `n * 0.5 + bias` (the bias given by its f32 bits): decoded `(v - bias) * 2`.
31    XyzBiased {
32        /// The bias, as the bits of an `f32`.
33        bias_bits: u32,
34    },
35    /// Octahedral mapping in lanes 0 and 1: each stored as `e * stored.scale + stored.bias` (the producer's scale and
36    /// bias, as measured) and read back by the engine as `v * read.scale + read.bias` before the unfold (`z = 1 - |x| -
37    /// |y|`, x and y folded where z < 0, normalised). The two halves are stated as each side measures them and need not
38    /// be exact inverses: an engine may store `0.4995 e + 0.4995` and read `2 v - 1`. The common 0-1 layout is `stored`
39    /// [`LaneAffine::UNORM`] and `read` [`LaneAffine::SIGNED`].
40    Octahedral {
41        /// The map from the unfolded coordinate to the stored value.
42        stored: LaneAffine,
43        /// The map from the stored value to the coordinate the unfold takes.
44        read: LaneAffine,
45    },
46    /// Octahedral in the diamond layout over three 8-bit UNORM lanes: lanes 0 and 1 are the low eight bits of two 9-bit
47    /// coordinates `u` and `v` (`round(255 (c + 1))`, 0 to 510 over -1 to 1), lane 2's bit 1 and bit 2 their ninth bits and
48    /// its bit 0 the sign of z; the lane's other five bits are not part of the normal (a payload byte, which an engine
49    /// may keep a class in). `x = (u + v) / 2`, `y = (u - v) / 2`, `|z| = 1 - |x| - |y|`, normalised
50    /// ([`NormalEncoding::decode_octahedral_diamond`]).
51    OctahedralDiamond,
52    /// The normal by its major axis, over four lanes. The face is D3D's cube-map face of the axis whose component is
53    /// largest in magnitude (`2 * axis + 1` where that component is negative: +x 0, -x 1, +y 2, -y 3, +z 4, -z 5), and
54    /// lanes 0 and 1 are that face's `sc` and `tc` components of the normal, not divided by the major component (+x
55    /// `(-z, -y)`, -x `(z, -y)`, +y `(x, z)`, -y `(x, -z)`, +z `(x, -y)`, -z `(-x, -y)`), each stored as `e *
56    /// minor.scale + minor.bias`. Lane 2 is the major component's sign by its side of one half (below for a positive
57    /// face, above for a negative one) and a payload `p` in 0 to 1 by its distance from one half, `p * payload.scale +
58    /// payload.bias`; the payload is not part of the normal (a value an engine keeps beside it). Lane 3 is the axis
59    /// over three (x 0, y 1/3, z 2/3). The major component's magnitude is the unit length's remainder
60    /// ([`NormalEncoding::decode_cube_face`]).
61    CubeFace {
62        /// The map applied to the `sc` and `tc` components for lanes 0 and 1.
63        minor: LaneAffine,
64        /// The map applied to the payload value in lane 2.
65        payload: LaneAffine,
66    },
67}
68/// One lane's affine map, `v * scale + bias`, by the f32 bits of each so that it compares and hashes exactly; it is written
69/// as the numbers `scale` and `bias`.
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub struct LaneAffine {
72    /// The scale, as the bits of an `f32`.
73    pub scale_bits: u32,
74    /// The bias, as the bits of an `f32`.
75    pub bias_bits: u32,
76}
77impl Serialize for LaneAffine {
78    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
79        use serde::ser::SerializeStruct;
80        let mut out = serializer.serialize_struct("LaneAffine", 2)?;
81        out.serialize_field("scale", &self.scale())?;
82        out.serialize_field("bias", &self.bias())?;
83        out.end()
84    }
85}
86impl LaneAffine {
87    /// A signed value stored in 0 to 1: `v * 0.5 + 0.5`.
88    pub const UNORM: Self = Self {
89        scale_bits: 0x3F00_0000,
90        bias_bits: 0x3F00_0000,
91    };
92
93    /// A value in 0 to 1 read back as signed: `v * 2 - 1`.
94    pub const SIGNED: Self = Self {
95        scale_bits: 0x4000_0000,
96        bias_bits: 0xBF80_0000,
97    };
98
99    /// A map with this scale and bias.
100    pub fn new(scale: f32, bias: f32) -> Self {
101        Self {
102            scale_bits: scale.to_bits(),
103            bias_bits: bias.to_bits(),
104        }
105    }
106
107    /// The scale.
108    pub fn scale(&self) -> f32 {
109        f32::from_bits(self.scale_bits)
110    }
111
112    /// The bias.
113    pub fn bias(&self) -> f32 {
114        f32::from_bits(self.bias_bits)
115    }
116
117    /// `v * scale + bias`.
118    pub fn apply(&self, v: f32) -> f32 {
119        v * self.scale() + self.bias()
120    }
121}
122/// An axis of a normal.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub(super) enum Axis {
125    X,
126    Y,
127    Z,
128}
129impl Axis {
130    pub(super) const ALL: [Axis; 3] = [Axis::X, Axis::Y, Axis::Z];
131
132    /// This axis's component of a vector.
133    pub(super) fn of<T: Copy>(self, [x, y, z]: [T; 3]) -> T {
134        match self {
135            Axis::X => x,
136            Axis::Y => y,
137            Axis::Z => z,
138        }
139    }
140}
141
142/// A face of the cube map of [`NormalEncoding::CubeFace`]: the axis of the normal's major component and, for lanes 0
143/// and 1, the axis and sign of the normal's component each holds.
144#[derive(Clone, Copy, Debug)]
145pub(super) struct CubeFace {
146    pub(super) major: Axis,
147    pub(super) minor: [(Axis, f64); 2],
148}
149impl CubeFace {
150    /// The face whose major component is on `axis`, facing the negative side or not.
151    pub(super) fn new(axis: Axis, negative: bool) -> CubeFace {
152        use Axis::{X, Y, Z};
153        let (major, minor) = match (axis, negative) {
154            (X, false) => (X, [(Z, -1.0), (Y, -1.0)]),
155            (X, true) => (X, [(Z, 1.0), (Y, -1.0)]),
156            (Y, false) => (Y, [(X, 1.0), (Z, 1.0)]),
157            (Y, true) => (Y, [(X, 1.0), (Z, -1.0)]),
158            (Z, false) => (Z, [(X, 1.0), (Y, -1.0)]),
159            (Z, true) => (Z, [(X, -1.0), (Y, -1.0)]),
160        };
161        CubeFace { major, minor }
162    }
163
164    /// A vector with `major` on the major axis, `first` and `second` on the axes lanes 0 and 1 hold, `none` on the other.
165    pub(super) fn place<T: Copy>(&self, major: T, first: T, second: T, none: T) -> [T; 3] {
166        let [(a, _), (b, _)] = self.minor;
167        Axis::ALL.map(|axis| {
168            if axis == self.major {
169                major
170            } else if axis == a {
171                first
172            } else if axis == b {
173                second
174            } else {
175                none
176            }
177        })
178    }
179}
180/// The 8-bit value an UNORM lane rounds to.
181#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // clamped to 0..=255 first
182fn unorm8(x: f32) -> u8 {
183    (x.clamp(0.0, 1.0) * 255.0).round_ties_even() as u8
184}
185
186/// `v` rounded to a whole number of 0 to 510 (nine bits less one).
187#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // clamped to 0..=510 first
188fn nine_bits(v: f64) -> u16 {
189    v.round().clamp(0.0, 510.0) as u16
190}
191
192/// `v` as an f32, rounded to the nearest.
193#[allow(clippy::cast_possible_truncation)] // the rounding is the point; the caller's values are lane values
194pub(super) fn narrow(v: f64) -> f32 {
195    v as f32
196}
197impl NormalEncoding {
198    /// The unit normal and the payload of four lanes stored as [`NormalEncoding::CubeFace`] with `minor` and
199    /// `payload`; `steps` is each lane's quantisation step in the target's format (1/1023 for a 10-bit UNORM lane,
200    /// 1/3 for a 2-bit one). Lane 3 is read as the nearest of 0, 1/3 and 2/3, and lane 2's side of one half as the
201    /// major component's sign; a lane 3 not within half its step of one of them, or a lane 2 within one of its steps of
202    /// one half, cannot be placed and is refused.
203    pub fn decode_cube_face(
204        minor: LaneAffine,
205        payload: LaneAffine,
206        lanes: [f32; 4],
207        steps: [f32; 4],
208    ) -> Result<([f64; 3], f32), String> {
209        let [a, b, side, lane_3] = lanes;
210        let thirds = f64::from(lane_3) * 3.0;
211        let nearest = thirds.round();
212        let [_, _, step_side, step_axis] = steps;
213        // by an exact comparison: a NaN, or an axis past z, is no axis
214        let axis_index = [0.0, 1.0, 2.0].iter().position(|&candidate| candidate == nearest);
215        let axis = axis_index
216            .and_then(|i| Axis::ALL.get(i))
217            .filter(|_| (thirds - nearest).abs() / 3.0 < f64::from(step_axis) / 2.0);
218        let Some(&axis) = axis else {
219            return Err(format!(
220                "a cube-face lane 3 of {lane_3} is not an axis (0, 1/3 or 2/3 within half a step of {step_axis})"
221            ));
222        };
223        let from_half = f64::from(side) - 0.5;
224        if from_half.abs() < f64::from(step_side) {
225            return Err(format!(
226                "a cube-face lane 2 of {side} is within a step ({step_side}) of one half: its sign cannot be read"
227            ));
228        }
229        let negative = from_half > 0.0;
230        let face = CubeFace::new(axis, negative);
231        let unmap = |v: f32| (f64::from(v) - f64::from(minor.bias())) / f64::from(minor.scale());
232        let (sc, tc) = (unmap(a), unmap(b));
233        let [(_, first_sign), (_, second_sign)] = face.minor;
234        let depth = (1.0 - sc * sc - tc * tc).max(0.0).sqrt() * if negative { -1.0 } else { 1.0 };
235        let n = face.place(depth, sc * first_sign, tc * second_sign, 0.0);
236        let length = n.iter().map(|c| c * c).sum::<f64>().sqrt();
237        let p = ((narrow(from_half.abs()) - payload.bias()) / payload.scale()).clamp(0.0, 1.0);
238        Ok((n.map(|c| c / length), p))
239    }
240
241    /// The four lanes [`NormalEncoding::CubeFace`] with `minor` and `payload` stores the unit normal `normal` and the
242    /// payload `p` (0 to 1) as, unquantised. The major axis is z where its magnitude is at least x's and y's, else y
243    /// where it is at least x's, else x (on an edge between faces either face decodes to the normal).
244    pub fn encode_cube_face(minor: LaneAffine, payload: LaneAffine, normal: [f32; 3], p: f32) -> [f32; 4] {
245        let [x, y, z] = normal;
246        let (axis, thirds) = if z.abs() >= x.abs() && z.abs() >= y.abs() {
247            (Axis::Z, 2.0)
248        } else if y.abs() >= x.abs() {
249            (Axis::Y, 1.0)
250        } else {
251            (Axis::X, 0.0)
252        };
253        let negative = axis.of(normal) < 0.0;
254        let [first, second] = CubeFace::new(axis, negative).minor;
255        let lane =
256            |(of, sign): (Axis, f64)| minor.apply(if sign < 0.0 { -of.of(normal) } else { of.of(normal) });
257        let offset = p * payload.scale() + payload.bias();
258        [
259            lane(first),
260            lane(second),
261            if negative { 0.5 + offset } else { 0.5 - offset },
262            thirds / 3.0,
263        ]
264    }
265
266    /// The normal and the payload byte of three UNORM lanes stored as [`NormalEncoding::OctahedralDiamond`]: each lane
267    /// is taken as the 8-bit value it rounds to.
268    pub fn decode_octahedral_diamond(lanes: [f32; 3]) -> ([f64; 3], u8) {
269        let [r, g, b] = lanes.map(unorm8);
270        let u9 = f64::from(b >> 1 & 1) * 256.0 + f64::from(r);
271        let v9 = f64::from(b >> 2 & 1) * 256.0 + f64::from(g);
272        let pu = (u9 / 255.0 - 1.0).clamp(-1.0, 1.0);
273        let pv = (v9 / 255.0 - 1.0).clamp(-1.0, 1.0);
274        let (x, y) = ((pu + pv) / 2.0, (pu - pv) / 2.0);
275        let z = (1.0 - x.abs() - y.abs()) * if b & 1 == 1 { 1.0 } else { -1.0 };
276        let n = (x * x + y * y + z * z).sqrt();
277        ([x / n, y / n, z / n], b >> 3)
278    }
279
280    /// The three 8-bit lanes [`NormalEncoding::OctahedralDiamond`] stores a normal and a payload byte (below 32) as;
281    /// a z of 0 stores as positive. A vector that is not zero is normalised by the encoding itself.
282    pub fn encode_octahedral_diamond(normal: [f64; 3], payload: u8) -> [u8; 3] {
283        let [x, y, z] = normal;
284        let sum = x.abs() + y.abs() + z.abs();
285        let (x, y) = (x / sum, y / sum);
286        let nine = |c: f64| nine_bits(c * 255.0 + 255.0);
287        let (u9, v9) = (nine(x + y), nine(x - y));
288        let sign = u8::from(z >= 0.0);
289        [
290            (u9 & 0xff) as u8,
291            (v9 & 0xff) as u8,
292            sign | ((u9 >> 8) as u8) << 1 | ((v9 >> 8) as u8) << 2 | (payload & 0x1f) << 3,
293        ]
294    }
295}
296/// How a scalar is stored in the lanes of an output: `stored = (value / scale)^(root / power)`, so the value is
297/// `stored^(power / root) * scale` (the scale given by its f32 bits). Power and root 1 are linear; power 2 a square root
298/// stored (a value kept in a perceptual range), 4 a fourth root; root 2 a square stored (a value the lighting takes the
299/// square root of).
300#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
301pub struct ScalarEncoding {
302    /// The power the stored value is raised to, to give the value over the scale: 1 for none, 2 for the square root
303    /// stored, 4 for the fourth root. Never 0.
304    pub power: u8,
305    /// The root taken of the stored value raised to `power`: 1 for none, 2 for the square stored. Never 0.
306    pub root: u8,
307    /// The scale's f32 bits (1.0 for none): bits so the encoding compares and hashes exactly; it is written as the
308    /// number `scale`.
309    pub scale_bits: u32,
310}
311impl Serialize for ScalarEncoding {
312    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
313        use serde::ser::SerializeStruct;
314        // `root` is written only where it is not 1, so an encoding without one reads as it always has
315        let mut out = serializer.serialize_struct(
316            "ScalarEncoding",
317            2usize.saturating_add(usize::from(self.root != 1)),
318        )?;
319        out.serialize_field("power", &self.power)?;
320        if self.root != 1 {
321            out.serialize_field("root", &self.root)?;
322        } else {
323            out.skip_field("root")?;
324        }
325        out.serialize_field("scale", &f32::from_bits(self.scale_bits))?;
326        out.end()
327    }
328}
329impl ScalarEncoding {
330    /// The value as it is (scale 1).
331    pub const LINEAR: Self = Self {
332        power: 1,
333        root: 1,
334        scale_bits: 0x3F80_0000,
335    };
336
337    /// Stored as the value over `scale`, no power.
338    pub fn linear_over(scale: f32) -> Self {
339        Self::power_of(1, scale)
340    }
341
342    /// Stored as the square root of the value over `scale`.
343    pub fn square_root_of(scale: f32) -> Self {
344        Self::power_of(2, scale)
345    }
346
347    /// Stored as the `power`th root of the value over `scale` (`power` 0 is taken as 1: no power).
348    pub fn power_of(power: u8, scale: f32) -> Self {
349        Self {
350            power: power.max(1),
351            root: 1,
352            scale_bits: scale.to_bits(),
353        }
354    }
355
356    /// Stored as the square of the value over `scale`: the value is the stored one's square root times `scale`.
357    pub fn square_of(scale: f32) -> Self {
358        Self {
359            power: 1,
360            root: 2,
361            scale_bits: scale.to_bits(),
362        }
363    }
364
365    /// The value for a stored one (a root of a negative stored value is taken of 0).
366    pub fn decode(&self, stored: f32) -> f32 {
367        let raised = stored.powi(i32::from(self.power));
368        let value = match self.root {
369            0 | 1 => raised,
370            2 => raised.max(0.0).sqrt(),
371            root => raised.max(0.0).powf(1.0 / f32::from(root)),
372        };
373        value * f32::from_bits(self.scale_bits)
374    }
375}
376impl ScalarEncoding {
377    /// Whether the encoding is one: a power and a root other than 0, and a finite scale.
378    pub fn check(&self) -> Result<(), String> {
379        let scale = f32::from_bits(self.scale_bits);
380        if self.power == 0 || self.root == 0 || !scale.is_finite() {
381            return Err(format!(
382                "an encoding of power {}, root {} and scale {scale}: power and root are never 0, the scale is finite",
383                self.power, self.root
384            ));
385        }
386        Ok(())
387    }
388}
389
390#[cfg(test)]
391#[allow(clippy::unwrap_used, reason = "hand-worked values")]
392mod tests {
393    use super::*;
394
395    /// An octahedral encoding states both of its maps, by bits, and writes them as numbers; the 0-1 layout's maps are
396    /// the constants, and they invert each other.
397    #[test]
398    fn an_octahedral_encoding_states_its_stored_and_read_maps() {
399        assert_eq!(LaneAffine::UNORM, LaneAffine::new(0.5, 0.5));
400        assert_eq!(LaneAffine::SIGNED, LaneAffine::new(2.0, -1.0));
401        for e in [-1.0f32, -0.25, 0.0, 0.5, 1.0] {
402            assert_eq!(LaneAffine::SIGNED.apply(LaneAffine::UNORM.apply(e)), e);
403        }
404        let measured = NormalEncoding::Octahedral {
405            stored: LaneAffine::new(0.4995, 0.4995),
406            read: LaneAffine::SIGNED,
407        };
408        assert_ne!(
409            measured,
410            NormalEncoding::Octahedral {
411                stored: LaneAffine::UNORM,
412                read: LaneAffine::SIGNED,
413            }
414        );
415        assert_eq!(
416            serde_json::to_value(measured).unwrap(),
417            serde_json::json!({"octahedral": {
418                "stored": {"scale": 0.4995f32, "bias": 0.4995f32},
419                "read": {"scale": 2.0, "bias": -1.0},
420            }})
421        );
422    }
423    /// The cube-face store as the source's G-buffer program writes it (`minor` 511/1023 both, `payload` 0.49804688 and
424    /// 0.0014662757, the program's literals) gives the program's own output for the ten normals it was evaluated at
425    /// with a payload of 0, and decodes back to them; every face, the edges and corners between faces and a grid of
426    /// normals round-trip through 10-bit lanes and a 2-bit lane 3; a lane 3 that is not an axis, and a lane 2 within a
427    /// step of one half, are refused.
428    #[test]
429    fn the_cube_face_decode_inverts_its_encoding_and_the_measured_lanes() {
430        let k = 511.0f32 / 1023.0;
431        let minor = LaneAffine::new(k, k);
432        assert_eq!(minor.scale_bits, 0x3EFF_BFF0);
433        let payload = LaneAffine::new(0.498_046_88, 0.001_466_275_7);
434        let steps = [1.0 / 1023.0, 1.0 / 1023.0, 1.0 / 1023.0, 1.0 / 3.0];
435        let encoding = NormalEncoding::CubeFace { minor, payload };
436        assert_eq!(
437            serde_json::to_value(encoding).unwrap()["cube_face"]["minor"]["scale"],
438            serde_json::json!(k)
439        );
440        // the program's T0 for each normal (its TEXCOORD2), as the evaluation printed it
441        let measured: [([f32; 3], [f32; 4]); 10] = [
442            ([0.36, 0.48, 0.8], [0.679335, 0.259746, 0.498534, 0.666667]),
443            ([0.36, 0.48, -0.8], [0.319687, 0.259746, 0.501466, 0.666667]),
444            ([-0.48, 0.36, -0.8], [0.739277, 0.319687, 0.501466, 0.666667]),
445            ([0.8, 0.36, 0.48], [0.259746, 0.319687, 0.498534, 0.0]),
446            ([-0.8, 0.36, -0.48], [0.259746, 0.319687, 0.501466, 0.0]),
447            ([0.36, 0.8, -0.48], [0.679335, 0.259746, 0.498534, 0.333333]),
448            ([-0.48, -0.8, 0.36], [0.259746, 0.319687, 0.501466, 0.333333]),
449            ([0.0, 0.0, -1.0], [0.499511, 0.499511, 0.501466, 0.666667]),
450            ([0.0, 0.0, 1.0], [0.499511, 0.499511, 0.498534, 0.666667]),
451            ([-0.6, 0.0, -0.8], [0.799218, 0.499511, 0.501466, 0.666667]),
452        ];
453        for (n, t0) in measured {
454            let lanes = NormalEncoding::encode_cube_face(minor, payload, n, 0.0);
455            assert!(
456                lanes.iter().zip(&t0).all(|(a, b)| (a - b).abs() < 1e-6),
457                "{n:?}: {lanes:?} against {t0:?}"
458            );
459            let (back, p) = NormalEncoding::decode_cube_face(minor, payload, t0, steps).unwrap();
460            let dot: f64 = back.iter().zip(&n).map(|(a, b)| a * f64::from(*b)).sum();
461            assert!(dot > 1.0 - 1e-6 && p < 1e-3, "{n:?}: {back:?} payload {p}");
462        }
463        let quantise = |lanes: [f32; 4]| {
464            let q = |v: f32, levels: f32| (v * levels).round() / levels;
465            [
466                q(lanes[0], 1023.0),
467                q(lanes[1], 1023.0),
468                q(lanes[2], 1023.0),
469                q(lanes[3], 3.0),
470            ]
471        };
472        let round_trip = |n: [f64; 3], p: f32| {
473            let length = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
474            let n = n.map(|c| (c / length) as f32);
475            let lanes = quantise(NormalEncoding::encode_cube_face(minor, payload, n, p));
476            let (back, read) = NormalEncoding::decode_cube_face(minor, payload, lanes, steps).unwrap();
477            let dot: f64 = back.iter().zip(&n).map(|(a, b)| a * f64::from(*b)).sum();
478            (dot, (read - p).abs())
479        };
480        // the six faces, the twelve edges and the eight corners between them, with payloads 0, 1/2 and 1
481        let mut worst = (1.0f64, 0.0f32);
482        for x in [-1.0, 0.0, 1.0] {
483            for y in [-1.0, 0.0, 1.0] {
484                for z in [-1.0, 0.0, 1.0] {
485                    if [x, y, z] == [0.0; 3] {
486                        continue;
487                    }
488                    for p in [0.0, 0.5, 1.0] {
489                        let (dot, off) = round_trip([x, y, z], p);
490                        worst = (worst.0.min(dot), worst.1.max(off));
491                    }
492                }
493            }
494        }
495        // and a grid over the sphere
496        for i in 0..40 {
497            for j in 0..80 {
498                let (theta, phi) = (
499                    std::f64::consts::PI * (f64::from(i) + 0.5) / 40.0,
500                    std::f64::consts::TAU * f64::from(j) / 80.0,
501                );
502                let (dot, off) = round_trip(
503                    [theta.sin() * phi.cos(), theta.sin() * phi.sin(), theta.cos()],
504                    0.25,
505                );
506                worst = (worst.0.min(dot), worst.1.max(off));
507            }
508        }
509        // a 10-bit step of a minor component is about 2/1023: the direction is within a few of them
510        assert!(
511            worst.0 > 1.0 - 2e-5 && worst.1 < 2.0 / 1023.0,
512            "worst dot {}, payload off {}",
513            worst.0,
514            worst.1
515        );
516        let refused =
517            |lanes: [f32; 4]| NormalEncoding::decode_cube_face(minor, payload, lanes, steps).unwrap_err();
518        assert!(refused([0.5, 0.5, 0.49, 1.0]).contains("is not an axis"));
519        // between two thirds in a lane finer than 2 bits (a 10-bit lane 3)
520        let fine = [steps[0], steps[1], steps[2], 1.0 / 1023.0];
521        assert!(
522            NormalEncoding::decode_cube_face(minor, payload, [0.5, 0.5, 0.49, 0.5], fine)
523                .unwrap_err()
524                .contains("is not an axis")
525        );
526        assert!(
527            NormalEncoding::decode_cube_face(minor, payload, [0.5, 0.5, 0.49, 341.0 / 1023.0], fine).is_ok()
528        );
529        assert!(refused([0.5, 0.5, 0.5 + 0.5 / 1023.0, 0.0]).contains("its sign cannot be read"));
530    }
531    /// The diamond octahedral store round-trips: the hand-worked axes (+z stores x = y = 255 and the sign bit; +x
532    /// stores u = v = 510, both ninth bits; -y stores u = 0, v = 510), the payload kept, and a grid of normals to
533    /// the 9-bit step.
534    #[test]
535    fn the_diamond_octahedral_decode_inverts_its_encoding() {
536        let unorm = |lanes: [u8; 3]| lanes.map(|v| f32::from(v) / 255.0);
537        let near = |a: [f64; 3], b: [f64; 3]| a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-9);
538        let (n, class) = NormalEncoding::decode_octahedral_diamond(unorm([255, 255, 5 << 3 | 1]));
539        assert!(near(n, [0.0, 0.0, 1.0]) && class == 5, "{n:?}");
540        let (n, _) = NormalEncoding::decode_octahedral_diamond(unorm([254, 254, 5 << 3 | 0b111]));
541        assert!(near(n, [1.0, 0.0, 0.0]), "{n:?}");
542        let (n, _) = NormalEncoding::decode_octahedral_diamond(unorm([0, 254, 5 << 3 | 0b101]));
543        assert!(near(n, [0.0, -1.0, 0.0]), "{n:?}");
544        assert_eq!(
545            NormalEncoding::encode_octahedral_diamond([0.0, 0.0, 1.0], 5),
546            [255, 255, 5 << 3 | 1]
547        );
548        assert_eq!(
549            NormalEncoding::encode_octahedral_diamond([1.0, 0.0, 0.0], 5),
550            [254, 254, 5 << 3 | 0b111]
551        );
552        assert_eq!(
553            NormalEncoding::encode_octahedral_diamond([0.0, -1.0, 0.0], 5),
554            [0, 254, 5 << 3 | 0b101]
555        );
556        // a grid over the sphere, both hemispheres, every payload class that matters
557        let mut worst = 1.0f64;
558        for i in 0..40 {
559            for j in 0..80 {
560                let (theta, phi) = (
561                    std::f64::consts::PI * (f64::from(i) + 0.5) / 40.0,
562                    std::f64::consts::TAU * f64::from(j) / 80.0,
563                );
564                let n = [theta.sin() * phi.cos(), theta.sin() * phi.sin(), theta.cos()];
565                let class = (i % 6) as u8;
566                let lanes = NormalEncoding::encode_octahedral_diamond(n, class);
567                let (back, payload) = NormalEncoding::decode_octahedral_diamond(unorm(lanes));
568                assert_eq!(payload, class);
569                worst = worst.min(n[0] * back[0] + n[1] * back[1] + n[2] * back[2]);
570            }
571        }
572        // a 9-bit coordinate step is 2/510: the direction is within a few of them
573        assert!(worst > 1.0 - 2e-4, "worst dot {worst}");
574    }
575    #[test]
576    fn a_scalar_encoding_decodes_what_it_stores() {
577        // stored = sqrt(value / 512): the value is the stored one squared times 512
578        let e = ScalarEncoding::square_root_of(512.0);
579        assert_eq!(e.decode(0.5), 128.0);
580        assert_eq!(ScalarEncoding::LINEAR.decode(0.25), 0.25);
581        assert_eq!(ScalarEncoding::square_root_of(1.0).decode(0.5), 0.25);
582        // a fourth root over 4: the value is the stored one to the fourth, times 4
583        let fourth = ScalarEncoding::power_of(4, 4.0);
584        assert_eq!(fourth.decode(0.5), 0.25);
585        assert_eq!(ScalarEncoding::linear_over(16.0).decode(0.25), 4.0);
586        // written as the number it is, not its bit pattern
587        assert_eq!(
588            serde_json::to_value(fourth).unwrap(),
589            serde_json::json!({"power": 4, "scale": 4.0})
590        );
591        assert_eq!(
592            serde_json::to_value(ScalarEncoding::linear_over(0.0625)).unwrap(),
593            serde_json::json!({"power": 1, "scale": 0.0625})
594        );
595        // a power of 0 is no power
596        assert_eq!(ScalarEncoding::power_of(0, 2.0), ScalarEncoding::linear_over(2.0));
597        // a square stored over 0.95: the value is the square root of the stored one, times 0.95; written with its root
598        let square = ScalarEncoding::square_of(0.95);
599        assert_eq!(square.decode(0.25), 0.475);
600        assert_eq!(square.decode(-0.25), 0.0);
601        assert_eq!(
602            serde_json::to_value(square).unwrap(),
603            serde_json::json!({"power": 1, "root": 2, "scale": 0.95f32})
604        );
605    }
606}