Skip to main content

azul_core/
transform.rs

1//! 3D transform matrix computations for CSS transforms.
2//!
3//! This module implements 4x4 transformation matrices for CSS `transform` properties,
4//! including translation, rotation, scaling, skewing, and perspective. It handles conversion
5//! from CSS transform functions to hardware-accelerated matrices for WebRender.
6//!
7//! On x86_64 platforms, the module automatically detects and uses SSE/AVX instructions
8//! for optimized matrix multiplication and inversion.
9//!
10//! **NOTE**: Matrices are stored in **row-major** format (unlike some graphics APIs that
11//! use column-major). The module handles coordinate system differences between WebRender
12//! and hit-testing via the `RotationMode` enum.
13
14use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
15
16use azul_css::props::style::{StyleTransform, StyleTransformOrigin};
17
18use crate::geom::LogicalPosition;
19
20/// CPU feature detection: true if initialization has been performed
21pub static INITIALIZED: AtomicBool = AtomicBool::new(false);
22/// CPU feature detection: true if AVX instructions are available
23pub static USE_AVX: AtomicBool = AtomicBool::new(false);
24/// CPU feature detection: true if SSE instructions are available
25pub static USE_SSE: AtomicBool = AtomicBool::new(false);
26
27/// Specifies the coordinate system convention for rotations.
28///
29/// `WebRender` uses a different rotation direction than hit-testing, so transforms
30/// must be adjusted based on their use case. This enum controls whether the
31/// rotation matrix is inverted to match the expected behavior.
32#[derive(Debug, Copy, Clone)]
33pub enum RotationMode {
34    /// Use rotation convention for `WebRender` (counter-clockwise, requires inversion)
35    ForWebRender,
36    /// Use rotation convention for hit-testing (clockwise, no inversion)
37    ForHitTesting,
38}
39
40/// A computed 4x4 transformation matrix in pixel space.
41///
42/// Represents the final transformation matrix for a DOM element after applying
43/// all CSS transform functions (translate, rotate, scale, etc.) and accounting
44/// for transform-origin.
45///
46/// # Memory Layout
47///
48/// Matrix is stored in **row-major** format:
49/// ```text
50/// m[0] = [m11, m12, m13, m14]
51/// m[1] = [m21, m22, m23, m24]
52/// m[2] = [m31, m32, m33, m34]
53/// m[3] = [m41, m42, m43, m44]
54/// ```
55#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
56#[repr(C)]
57pub struct ComputedTransform3D {
58    /// The 4x4 matrix in row-major format
59    pub m: [[f32; 4]; 4],
60}
61
62impl ComputedTransform3D {
63    /// The identity matrix (no transformation).
64    pub const IDENTITY: Self = Self {
65        m: [
66            [1.0, 0.0, 0.0, 0.0],
67            [0.0, 1.0, 0.0, 0.0],
68            [0.0, 0.0, 1.0, 0.0],
69            [0.0, 0.0, 0.0, 1.0],
70        ],
71    };
72
73    /// Creates a new 4x4 transformation matrix with the given elements.
74    ///
75    /// Elements are specified in row-major order (m11, m12, ..., m44).
76    #[must_use] pub const fn new(
77        m11: f32,
78        m12: f32,
79        m13: f32,
80        m14: f32,
81        m21: f32,
82        m22: f32,
83        m23: f32,
84        m24: f32,
85        m31: f32,
86        m32: f32,
87        m33: f32,
88        m34: f32,
89        m41: f32,
90        m42: f32,
91        m43: f32,
92        m44: f32,
93    ) -> Self {
94        Self {
95            m: [
96                [m11, m12, m13, m14],
97                [m21, m22, m23, m24],
98                [m31, m32, m33, m34],
99                [m41, m42, m43, m44],
100            ],
101        }
102    }
103
104    /// Creates a 2D transformation matrix (3D matrix with Z = 0).
105    ///
106    /// This is equivalent to the CSS `matrix()` function. The transformation
107    /// only affects the X and Y axes.
108    ///
109    /// Corresponds to `matrix(m11, m12, m21, m22, m41, m42)` in CSS.
110    const fn new_2d(m11: f32, m12: f32, m21: f32, m22: f32, m41: f32, m42: f32) -> Self {
111        Self::new(
112            m11, m12, 0.0, 0.0, m21, m22, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, m41, m42, 0.0, 1.0,
113        )
114    }
115
116    /// Computes the inverse of this transformation matrix.
117    ///
118    /// This function uses a standard matrix inversion algorithm. Returns the
119    /// identity matrix if the determinant is zero (singular matrix).
120    ///
121    /// NOTE: This is a relatively expensive operation.
122    #[must_use]
123    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
124    pub fn inverse(&self) -> Self {
125        let det = self.determinant();
126
127        if det.abs() < f32::EPSILON {
128            return Self::IDENTITY;
129        }
130
131        let m = Self::new(
132            self.m[1][2] * self.m[2][3] * self.m[3][1] - self.m[1][3] * self.m[2][2] * self.m[3][1]
133                + self.m[1][3] * self.m[2][1] * self.m[3][2]
134                - self.m[1][1] * self.m[2][3] * self.m[3][2]
135                - self.m[1][2] * self.m[2][1] * self.m[3][3]
136                + self.m[1][1] * self.m[2][2] * self.m[3][3],
137            self.m[0][3] * self.m[2][2] * self.m[3][1]
138                - self.m[0][2] * self.m[2][3] * self.m[3][1]
139                - self.m[0][3] * self.m[2][1] * self.m[3][2]
140                + self.m[0][1] * self.m[2][3] * self.m[3][2]
141                + self.m[0][2] * self.m[2][1] * self.m[3][3]
142                - self.m[0][1] * self.m[2][2] * self.m[3][3],
143            self.m[0][2] * self.m[1][3] * self.m[3][1] - self.m[0][3] * self.m[1][2] * self.m[3][1]
144                + self.m[0][3] * self.m[1][1] * self.m[3][2]
145                - self.m[0][1] * self.m[1][3] * self.m[3][2]
146                - self.m[0][2] * self.m[1][1] * self.m[3][3]
147                + self.m[0][1] * self.m[1][2] * self.m[3][3],
148            self.m[0][3] * self.m[1][2] * self.m[2][1]
149                - self.m[0][2] * self.m[1][3] * self.m[2][1]
150                - self.m[0][3] * self.m[1][1] * self.m[2][2]
151                + self.m[0][1] * self.m[1][3] * self.m[2][2]
152                + self.m[0][2] * self.m[1][1] * self.m[2][3]
153                - self.m[0][1] * self.m[1][2] * self.m[2][3],
154            self.m[1][3] * self.m[2][2] * self.m[3][0]
155                - self.m[1][2] * self.m[2][3] * self.m[3][0]
156                - self.m[1][3] * self.m[2][0] * self.m[3][2]
157                + self.m[1][0] * self.m[2][3] * self.m[3][2]
158                + self.m[1][2] * self.m[2][0] * self.m[3][3]
159                - self.m[1][0] * self.m[2][2] * self.m[3][3],
160            self.m[0][2] * self.m[2][3] * self.m[3][0] - self.m[0][3] * self.m[2][2] * self.m[3][0]
161                + self.m[0][3] * self.m[2][0] * self.m[3][2]
162                - self.m[0][0] * self.m[2][3] * self.m[3][2]
163                - self.m[0][2] * self.m[2][0] * self.m[3][3]
164                + self.m[0][0] * self.m[2][2] * self.m[3][3],
165            self.m[0][3] * self.m[1][2] * self.m[3][0]
166                - self.m[0][2] * self.m[1][3] * self.m[3][0]
167                - self.m[0][3] * self.m[1][0] * self.m[3][2]
168                + self.m[0][0] * self.m[1][3] * self.m[3][2]
169                + self.m[0][2] * self.m[1][0] * self.m[3][3]
170                - self.m[0][0] * self.m[1][2] * self.m[3][3],
171            self.m[0][2] * self.m[1][3] * self.m[2][0] - self.m[0][3] * self.m[1][2] * self.m[2][0]
172                + self.m[0][3] * self.m[1][0] * self.m[2][2]
173                - self.m[0][0] * self.m[1][3] * self.m[2][2]
174                - self.m[0][2] * self.m[1][0] * self.m[2][3]
175                + self.m[0][0] * self.m[1][2] * self.m[2][3],
176            self.m[1][1] * self.m[2][3] * self.m[3][0] - self.m[1][3] * self.m[2][1] * self.m[3][0]
177                + self.m[1][3] * self.m[2][0] * self.m[3][1]
178                - self.m[1][0] * self.m[2][3] * self.m[3][1]
179                - self.m[1][1] * self.m[2][0] * self.m[3][3]
180                + self.m[1][0] * self.m[2][1] * self.m[3][3],
181            self.m[0][3] * self.m[2][1] * self.m[3][0]
182                - self.m[0][1] * self.m[2][3] * self.m[3][0]
183                - self.m[0][3] * self.m[2][0] * self.m[3][1]
184                + self.m[0][0] * self.m[2][3] * self.m[3][1]
185                + self.m[0][1] * self.m[2][0] * self.m[3][3]
186                - self.m[0][0] * self.m[2][1] * self.m[3][3],
187            self.m[0][1] * self.m[1][3] * self.m[3][0] - self.m[0][3] * self.m[1][1] * self.m[3][0]
188                + self.m[0][3] * self.m[1][0] * self.m[3][1]
189                - self.m[0][0] * self.m[1][3] * self.m[3][1]
190                - self.m[0][1] * self.m[1][0] * self.m[3][3]
191                + self.m[0][0] * self.m[1][1] * self.m[3][3],
192            self.m[0][3] * self.m[1][1] * self.m[2][0]
193                - self.m[0][1] * self.m[1][3] * self.m[2][0]
194                - self.m[0][3] * self.m[1][0] * self.m[2][1]
195                + self.m[0][0] * self.m[1][3] * self.m[2][1]
196                + self.m[0][1] * self.m[1][0] * self.m[2][3]
197                - self.m[0][0] * self.m[1][1] * self.m[2][3],
198            self.m[1][2] * self.m[2][1] * self.m[3][0]
199                - self.m[1][1] * self.m[2][2] * self.m[3][0]
200                - self.m[1][2] * self.m[2][0] * self.m[3][1]
201                + self.m[1][0] * self.m[2][2] * self.m[3][1]
202                + self.m[1][1] * self.m[2][0] * self.m[3][2]
203                - self.m[1][0] * self.m[2][1] * self.m[3][2],
204            self.m[0][1] * self.m[2][2] * self.m[3][0] - self.m[0][2] * self.m[2][1] * self.m[3][0]
205                + self.m[0][2] * self.m[2][0] * self.m[3][1]
206                - self.m[0][0] * self.m[2][2] * self.m[3][1]
207                - self.m[0][1] * self.m[2][0] * self.m[3][2]
208                + self.m[0][0] * self.m[2][1] * self.m[3][2],
209            self.m[0][2] * self.m[1][1] * self.m[3][0]
210                - self.m[0][1] * self.m[1][2] * self.m[3][0]
211                - self.m[0][2] * self.m[1][0] * self.m[3][1]
212                + self.m[0][0] * self.m[1][2] * self.m[3][1]
213                + self.m[0][1] * self.m[1][0] * self.m[3][2]
214                - self.m[0][0] * self.m[1][1] * self.m[3][2],
215            self.m[0][1] * self.m[1][2] * self.m[2][0] - self.m[0][2] * self.m[1][1] * self.m[2][0]
216                + self.m[0][2] * self.m[1][0] * self.m[2][1]
217                - self.m[0][0] * self.m[1][2] * self.m[2][1]
218                - self.m[0][1] * self.m[1][0] * self.m[2][2]
219                + self.m[0][0] * self.m[1][1] * self.m[2][2],
220        );
221
222        m.multiply_scalar(1.0 / det)
223    }
224
225    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
226    fn determinant(&self) -> f32 {
227        // Accumulate in f64. Individual f32 products (e.g. m[0][0]*m[1][1] on a
228        // diag(1e20) matrix = 1e40) overflow to ±inf BEFORE the legitimately-zero
229        // off-diagonal factors multiply in, and inf * 0 = NaN, poisoning the whole sum.
230        // f64 has the range to hold the products; the final cast saturates a real
231        // overflow to ±inf and propagates a NaN input as NaN.
232        let m = |i: usize, j: usize| f64::from(self.m[i][j]);
233        let det = m(0, 3) * m(1, 2) * m(2, 1) * m(3, 0)
234            - m(0, 2) * m(1, 3) * m(2, 1) * m(3, 0)
235            - m(0, 3) * m(1, 1) * m(2, 2) * m(3, 0)
236            + m(0, 1) * m(1, 3) * m(2, 2) * m(3, 0)
237            + m(0, 2) * m(1, 1) * m(2, 3) * m(3, 0)
238            - m(0, 1) * m(1, 2) * m(2, 3) * m(3, 0)
239            - m(0, 3) * m(1, 2) * m(2, 0) * m(3, 1)
240            + m(0, 2) * m(1, 3) * m(2, 0) * m(3, 1)
241            + m(0, 3) * m(1, 0) * m(2, 2) * m(3, 1)
242            - m(0, 0) * m(1, 3) * m(2, 2) * m(3, 1)
243            - m(0, 2) * m(1, 0) * m(2, 3) * m(3, 1)
244            + m(0, 0) * m(1, 2) * m(2, 3) * m(3, 1)
245            + m(0, 3) * m(1, 1) * m(2, 0) * m(3, 2)
246            - m(0, 1) * m(1, 3) * m(2, 0) * m(3, 2)
247            - m(0, 3) * m(1, 0) * m(2, 1) * m(3, 2)
248            + m(0, 0) * m(1, 3) * m(2, 1) * m(3, 2)
249            + m(0, 1) * m(1, 0) * m(2, 3) * m(3, 2)
250            - m(0, 0) * m(1, 1) * m(2, 3) * m(3, 2)
251            - m(0, 2) * m(1, 1) * m(2, 0) * m(3, 3)
252            + m(0, 1) * m(1, 2) * m(2, 0) * m(3, 3)
253            + m(0, 2) * m(1, 0) * m(2, 1) * m(3, 3)
254            - m(0, 0) * m(1, 2) * m(2, 1) * m(3, 3)
255            - m(0, 1) * m(1, 0) * m(2, 2) * m(3, 3)
256            + m(0, 0) * m(1, 1) * m(2, 2) * m(3, 3);
257        #[allow(clippy::cast_possible_truncation)] // determinant computed in f64, narrowed to the f32 public type
258        let det = det as f32;
259        det
260    }
261
262    fn multiply_scalar(&self, x: f32) -> Self {
263        Self::new(
264            self.m[0][0] * x,
265            self.m[0][1] * x,
266            self.m[0][2] * x,
267            self.m[0][3] * x,
268            self.m[1][0] * x,
269            self.m[1][1] * x,
270            self.m[1][2] * x,
271            self.m[1][3] * x,
272            self.m[2][0] * x,
273            self.m[2][1] * x,
274            self.m[2][2] * x,
275            self.m[2][3] * x,
276            self.m[3][0] * x,
277            self.m[3][1] * x,
278            self.m[3][2] * x,
279            self.m[3][3] * x,
280        )
281    }
282
283    /// Computes the matrix of a rect from a `&[StyleTransform]`.
284    pub fn from_style_transform_vec(
285        t_vec: &[StyleTransform],
286        transform_origin: &StyleTransformOrigin,
287        percent_resolve_x: f32,
288        percent_resolve_y: f32,
289        rotation_mode: RotationMode,
290    ) -> Self {
291        // Uses AVX or SSE SIMD when available on x86_64
292        //
293        // AUDIT-TODO: `USE_AVX`/`USE_SSE` are populated in `gpu.rs` from a raw
294        // CPUID leaf-1 feature bit (ECX[28] for AVX), which reports only that
295        // the CPU *implements* AVX — NOT that the OS has enabled the YMM state
296        // via XCR0 (XGETBV). On a kernel that didn't `XSETBV`-enable AVX, using
297        // these intrinsics faults with SIGILL. The robust gate is
298        // `is_x86_feature_detected!("avx")` / `("sse")`, which also checks the
299        // OS-enabled bit. That detection lives in `gpu.rs` (out of scope for
300        // this edit); consumers here rely on it having gated the flags. Prefer
301        // migrating the `gpu.rs` probe to `is_x86_feature_detected!`.
302        let mut matrix = Self::IDENTITY;
303        let use_avx =
304            INITIALIZED.load(AtomicOrdering::Relaxed) && USE_AVX.load(AtomicOrdering::Relaxed);
305        let use_sse = !use_avx
306            && INITIALIZED.load(AtomicOrdering::Relaxed)
307            && USE_SSE.load(AtomicOrdering::Relaxed);
308
309        if use_avx {
310            for t in t_vec {
311                // SAFETY: `use_avx` is only set when the AVX feature flag was
312                // detected (see AUDIT-TODO above), so calling the AVX intrinsics
313                // in `then_avx8` is legal on this CPU.
314                #[cfg(target_arch = "x86_64")]
315                unsafe {
316                    matrix = matrix.then_avx8(&Self::from_style_transform(
317                        t,
318                        transform_origin,
319                        percent_resolve_x,
320                        percent_resolve_y,
321                        rotation_mode,
322                    ));
323                }
324            }
325        } else if use_sse {
326            for t in t_vec {
327                // SAFETY: `use_sse` is only set when the SSE feature flag was
328                // detected (see AUDIT-TODO above), so calling the SSE intrinsics
329                // in `then_sse` is legal on this CPU.
330                #[cfg(target_arch = "x86_64")]
331                unsafe {
332                    matrix = matrix.then_sse(&Self::from_style_transform(
333                        t,
334                        transform_origin,
335                        percent_resolve_x,
336                        percent_resolve_y,
337                        rotation_mode,
338                    ));
339                }
340            }
341        } else {
342            // fallback for everything else
343            for t in t_vec {
344                matrix = matrix.then(&Self::from_style_transform(
345                    t,
346                    transform_origin,
347                    percent_resolve_x,
348                    percent_resolve_y,
349                    rotation_mode,
350                ));
351            }
352        }
353
354        matrix
355    }
356
357    /// Creates a new transform from a style transform using the
358    /// parent width as a way to resolve for percentages
359    #[allow(clippy::many_single_char_names)] // domain-standard colour/coordinate component names
360    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
361    fn from_style_transform(
362        t: &StyleTransform,
363        transform_origin: &StyleTransformOrigin,
364        percent_resolve_x: f32,
365        percent_resolve_y: f32,
366        rotation_mode: RotationMode,
367    ) -> Self {
368        use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
369        use azul_css::props::style::StyleTransform::{Matrix, Matrix3D, Translate, Translate3D, TranslateX, TranslateY, TranslateZ, Rotate3D, RotateX, RotateY, Rotate, RotateZ, Scale, Scale3D, ScaleX, ScaleY, ScaleZ, Skew, SkewX, SkewY, Perspective};
370        match t {
371            Matrix(mat2d) => {
372                let a = mat2d.a.get();
373                let b = mat2d.b.get();
374                let c = mat2d.c.get();
375                let d = mat2d.d.get();
376                let tx = mat2d.tx.get();
377                let ty = mat2d.ty.get();
378
379                Self::new_2d(a, b, c, d, tx, ty)
380            }
381            Matrix3D(mat3d) => {
382                let m11 = mat3d.m11.get();
383                let m12 = mat3d.m12.get();
384                let m13 = mat3d.m13.get();
385                let m14 = mat3d.m14.get();
386                let m21 = mat3d.m21.get();
387                let m22 = mat3d.m22.get();
388                let m23 = mat3d.m23.get();
389                let m24 = mat3d.m24.get();
390                let m31 = mat3d.m31.get();
391                let m32 = mat3d.m32.get();
392                let m33 = mat3d.m33.get();
393                let m34 = mat3d.m34.get();
394                let m41 = mat3d.m41.get();
395                let m42 = mat3d.m42.get();
396                let m43 = mat3d.m43.get();
397                let m44 = mat3d.m44.get();
398
399                Self::new(
400                    m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44,
401                )
402            }
403            Translate(trans2d) => {
404
405                Self::new_translation(
406                    trans2d
407                        .x
408                        .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
409                    trans2d
410                        .y
411                        .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
412                    0.0,
413                )
414            }
415            Translate3D(trans3d) => {
416
417                Self::new_translation(
418                    trans3d
419                        .x
420                        .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
421                    trans3d
422                        .y
423                        .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
424                    trans3d
425                        .z
426                        // CSS has no containing block for Z-axis percentages; use X as fallback
427                        .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
428                )
429            }
430            TranslateX(trans_x) => {
431
432                Self::new_translation(
433                    trans_x.to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
434                    0.0,
435                    0.0,
436                )
437            }
438            TranslateY(trans_y) => {
439
440                Self::new_translation(
441                    0.0,
442                    trans_y.to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
443                    0.0,
444                )
445            }
446            TranslateZ(trans_z) => {
447
448                Self::new_translation(
449                    0.0,
450                    0.0,
451                    trans_z.to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
452                )
453            } // CSS has no containing block for Z-axis percentages; use X as fallback
454            Rotate3D(rot3d) => {
455
456                let rotation_origin = (
457                    transform_origin
458                        .x
459                        .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
460                    transform_origin
461                        .y
462                        .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
463                );
464                Self::make_rotation(
465                    rotation_origin,
466                    rot3d.angle.to_degrees(),
467                    rot3d.x.get(),
468                    rot3d.y.get(),
469                    rot3d.z.get(),
470                    rotation_mode,
471                )
472            }
473            RotateX(angle_x) => {
474
475                let rotation_origin = (
476                    transform_origin
477                        .x
478                        .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
479                    transform_origin
480                        .y
481                        .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
482                );
483                Self::make_rotation(
484                    rotation_origin,
485                    angle_x.to_degrees(),
486                    1.0,
487                    0.0,
488                    0.0,
489                    rotation_mode,
490                )
491            }
492            RotateY(angle_y) => {
493
494                let rotation_origin = (
495                    transform_origin
496                        .x
497                        .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
498                    transform_origin
499                        .y
500                        .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
501                );
502                Self::make_rotation(
503                    rotation_origin,
504                    angle_y.to_degrees(),
505                    0.0,
506                    1.0,
507                    0.0,
508                    rotation_mode,
509                )
510            }
511            Rotate(angle_z) | RotateZ(angle_z) => {
512
513                let rotation_origin = (
514                    transform_origin
515                        .x
516                        .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
517                    transform_origin
518                        .y
519                        .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
520                );
521                Self::make_rotation(
522                    rotation_origin,
523                    angle_z.to_degrees(),
524                    0.0,
525                    0.0,
526                    1.0,
527                    rotation_mode,
528                )
529            }
530            Scale(scale2d) => Self::new_scale(scale2d.x.get(), scale2d.y.get(), 1.0),
531            Scale3D(scale3d) => Self::new_scale(scale3d.x.get(), scale3d.y.get(), scale3d.z.get()),
532            ScaleX(scale_x) => Self::new_scale(scale_x.normalized(), 1.0, 1.0),
533            ScaleY(scale_y) => Self::new_scale(1.0, scale_y.normalized(), 1.0),
534            ScaleZ(scale_z) => Self::new_scale(1.0, 1.0, scale_z.normalized()),
535            Skew(skew2d) => Self::new_skew(skew2d.x.to_degrees(), skew2d.y.to_degrees()),
536            SkewX(skew_x) => Self::new_skew(skew_x.to_degrees(), 0.0),
537            SkewY(skew_y) => Self::new_skew(0.0, skew_y.to_degrees()),
538            Perspective(px) => {
539
540                Self::new_perspective(px.to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE))
541            }
542        }
543    }
544
545    /// Creates a scaling matrix with independent scale factors per axis.
546    #[must_use]
547    #[inline]
548    pub const fn new_scale(x: f32, y: f32, z: f32) -> Self {
549        Self::new(
550            x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, 1.0,
551        )
552    }
553
554    /// Creates a translation matrix that moves by `(x, y, z)`.
555    #[must_use]
556    #[inline]
557    pub const fn new_translation(x: f32, y: f32, z: f32) -> Self {
558        Self::new(
559            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0,
560        )
561    }
562
563    /// Creates a perspective projection matrix with distance `d`.
564    #[must_use]
565    #[inline]
566    fn new_perspective(d: f32) -> Self {
567        Self::new(
568            1.0,
569            0.0,
570            0.0,
571            0.0,
572            0.0,
573            1.0,
574            0.0,
575            0.0,
576            0.0,
577            0.0,
578            1.0,
579            -1.0 / d,
580            0.0,
581            0.0,
582            0.0,
583            1.0,
584        )
585    }
586
587    /// Create a 3d rotation transform from an angle / axis.
588    /// The supplied axis must be normalized.
589    #[must_use]
590    #[inline]
591    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
592    fn new_rotation(x: f32, y: f32, z: f32, theta_radians: f32) -> Self {
593        let xx = x * x;
594        let yy = y * y;
595        let zz = z * z;
596
597        let half_theta = theta_radians / 2.0;
598        let sc = half_theta.sin() * half_theta.cos();
599        let sq = half_theta.sin() * half_theta.sin();
600
601        Self::new(
602            1.0 - 2.0 * (yy + zz) * sq,
603            2.0 * (x * y * sq + z * sc),
604            2.0 * (x * z * sq - y * sc),
605            0.0,
606            2.0 * (x * y * sq - z * sc),
607            1.0 - 2.0 * (xx + zz) * sq,
608            2.0 * (y * z * sq + x * sc),
609            0.0,
610            2.0 * (x * z * sq + y * sc),
611            2.0 * (y * z * sq - x * sc),
612            1.0 - 2.0 * (xx + yy) * sq,
613            0.0,
614            0.0,
615            0.0,
616            0.0,
617            1.0,
618        )
619    }
620
621    /// Creates a 2D skew matrix from angles in degrees.
622    #[must_use]
623    #[inline]
624    fn new_skew(alpha: f32, beta: f32) -> Self {
625        let (sx, sy) = (beta.to_radians().tan(), alpha.to_radians().tan());
626        Self::new(
627            1.0, sx, 0.0, 0.0, sy, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
628        )
629    }
630
631    /// Returns this matrix transposed to column-major layout.
632    #[must_use]
633    pub(crate) const fn get_column_major(&self) -> Self {
634        Self::new(
635            self.m[0][0],
636            self.m[1][0],
637            self.m[2][0],
638            self.m[3][0],
639            self.m[0][1],
640            self.m[1][1],
641            self.m[2][1],
642            self.m[3][1],
643            self.m[0][2],
644            self.m[1][2],
645            self.m[2][2],
646            self.m[3][2],
647            self.m[0][3],
648            self.m[1][3],
649            self.m[2][3],
650            self.m[3][3],
651        )
652    }
653
654    /// Transforms a 2D point into the target coordinate space.
655    #[must_use]
656    pub fn transform_point2d(&self, p: LogicalPosition) -> Option<LogicalPosition> {
657        let w =
658            p.x.mul_add(self.m[0][3], p.y.mul_add(self.m[1][3], self.m[3][3]));
659
660        if !w.is_sign_positive() {
661            return None;
662        }
663
664        let x =
665            p.x.mul_add(self.m[0][0], p.y.mul_add(self.m[1][0], self.m[3][0]));
666        let y =
667            p.x.mul_add(self.m[0][1], p.y.mul_add(self.m[1][1], self.m[3][1]));
668
669        Some(LogicalPosition { x: x / w, y: y / w })
670    }
671
672    /// Scales the translation components of this matrix by `scale_factor` for DPI adjustment.
673    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
674        // only scale the translation, don't scale anything else
675        self.m[3][0] *= scale_factor;
676        self.m[3][1] *= scale_factor;
677        self.m[3][2] *= scale_factor;
678    }
679
680    /// Multiplies this matrix by `other`, applying `other` AFTER the current matrix.
681    #[must_use]
682    #[inline]
683    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
684    pub fn then(&self, other: &Self) -> Self {
685        Self::new(
686            self.m[0][0].mul_add(
687                other.m[0][0],
688                self.m[0][1].mul_add(
689                    other.m[1][0],
690                    self.m[0][2].mul_add(other.m[2][0], self.m[0][3] * other.m[3][0]),
691                ),
692            ),
693            self.m[0][0].mul_add(
694                other.m[0][1],
695                self.m[0][1].mul_add(
696                    other.m[1][1],
697                    self.m[0][2].mul_add(other.m[2][1], self.m[0][3] * other.m[3][1]),
698                ),
699            ),
700            self.m[0][0].mul_add(
701                other.m[0][2],
702                self.m[0][1].mul_add(
703                    other.m[1][2],
704                    self.m[0][2].mul_add(other.m[2][2], self.m[0][3] * other.m[3][2]),
705                ),
706            ),
707            self.m[0][0].mul_add(
708                other.m[0][3],
709                self.m[0][1].mul_add(
710                    other.m[1][3],
711                    self.m[0][2].mul_add(other.m[2][3], self.m[0][3] * other.m[3][3]),
712                ),
713            ),
714            self.m[1][0].mul_add(
715                other.m[0][0],
716                self.m[1][1].mul_add(
717                    other.m[1][0],
718                    self.m[1][2].mul_add(other.m[2][0], self.m[1][3] * other.m[3][0]),
719                ),
720            ),
721            self.m[1][0].mul_add(
722                other.m[0][1],
723                self.m[1][1].mul_add(
724                    other.m[1][1],
725                    self.m[1][2].mul_add(other.m[2][1], self.m[1][3] * other.m[3][1]),
726                ),
727            ),
728            self.m[1][0].mul_add(
729                other.m[0][2],
730                self.m[1][1].mul_add(
731                    other.m[1][2],
732                    self.m[1][2].mul_add(other.m[2][2], self.m[1][3] * other.m[3][2]),
733                ),
734            ),
735            self.m[1][0].mul_add(
736                other.m[0][3],
737                self.m[1][1].mul_add(
738                    other.m[1][3],
739                    self.m[1][2].mul_add(other.m[2][3], self.m[1][3] * other.m[3][3]),
740                ),
741            ),
742            self.m[2][0].mul_add(
743                other.m[0][0],
744                self.m[2][1].mul_add(
745                    other.m[1][0],
746                    self.m[2][2].mul_add(other.m[2][0], self.m[2][3] * other.m[3][0]),
747                ),
748            ),
749            self.m[2][0].mul_add(
750                other.m[0][1],
751                self.m[2][1].mul_add(
752                    other.m[1][1],
753                    self.m[2][2].mul_add(other.m[2][1], self.m[2][3] * other.m[3][1]),
754                ),
755            ),
756            self.m[2][0].mul_add(
757                other.m[0][2],
758                self.m[2][1].mul_add(
759                    other.m[1][2],
760                    self.m[2][2].mul_add(other.m[2][2], self.m[2][3] * other.m[3][2]),
761                ),
762            ),
763            self.m[2][0].mul_add(
764                other.m[0][3],
765                self.m[2][1].mul_add(
766                    other.m[1][3],
767                    self.m[2][2].mul_add(other.m[2][3], self.m[2][3] * other.m[3][3]),
768                ),
769            ),
770            self.m[3][0].mul_add(
771                other.m[0][0],
772                self.m[3][1].mul_add(
773                    other.m[1][0],
774                    self.m[3][2].mul_add(other.m[2][0], self.m[3][3] * other.m[3][0]),
775                ),
776            ),
777            self.m[3][0].mul_add(
778                other.m[0][1],
779                self.m[3][1].mul_add(
780                    other.m[1][1],
781                    self.m[3][2].mul_add(other.m[2][1], self.m[3][3] * other.m[3][1]),
782                ),
783            ),
784            self.m[3][0].mul_add(
785                other.m[0][2],
786                self.m[3][1].mul_add(
787                    other.m[1][2],
788                    self.m[3][2].mul_add(other.m[2][2], self.m[3][3] * other.m[3][2]),
789                ),
790            ),
791            self.m[3][0].mul_add(
792                other.m[0][3],
793                self.m[3][1].mul_add(
794                    other.m[1][3],
795                    self.m[3][2].mul_add(other.m[2][3], self.m[3][3] * other.m[3][3]),
796                ),
797            ),
798        )
799    }
800
801    // credit: https://gist.github.com/rygorous/4172889
802
803    // linear combination:
804    // a[0] * B.row[0] + a[1] * B.row[1] + a[2] * B.row[2] + a[3] * B.row[3]
805    //
806    // SAFETY: the caller must guarantee SSE is available on this CPU (see the
807    // `use_sse` gate in `from_style_transform_vec`). Every `mem::transmute` here
808    // is a BY-VALUE `[f32; 4]` -> `__m128` conversion: both types are 16 bytes
809    // and the value is moved through a register, so no *reference* to under-
810    // aligned storage is ever formed and there is no alignment invariant to
811    // violate (unlike the AVX broadcast, which must use an unaligned load).
812    #[cfg(target_arch = "x86_64")]
813    #[inline]
814    unsafe fn linear_combine_sse(a: [f32; 4], b: &Self) -> [f32; 4] { unsafe {
815        use core::{
816            arch::x86_64::{__m128, _mm_add_ps, _mm_mul_ps, _mm_shuffle_ps},
817            mem,
818        };
819
820        let a: __m128 = mem::transmute(a);
821        let mut result = _mm_mul_ps(_mm_shuffle_ps(a, a, 0x00), mem::transmute::<[f32; 4], __m128>(b.m[0]));
822        result = _mm_add_ps(
823            result,
824            _mm_mul_ps(_mm_shuffle_ps(a, a, 0x55), mem::transmute::<[f32; 4], __m128>(b.m[1])),
825        );
826        result = _mm_add_ps(
827            result,
828            _mm_mul_ps(_mm_shuffle_ps(a, a, 0xaa), mem::transmute::<[f32; 4], __m128>(b.m[2])),
829        );
830        result = _mm_add_ps(
831            result,
832            _mm_mul_ps(_mm_shuffle_ps(a, a, 0xff), mem::transmute::<[f32; 4], __m128>(b.m[3])),
833        );
834
835        mem::transmute(result)
836    }}
837
838    /// Multiplies this matrix by `other` using SSE instructions.
839    ///
840    /// SAFETY: caller must guarantee SSE is available; only forwards to
841    /// `linear_combine_sse`, whose safety contract is identical.
842    #[cfg(target_arch = "x86_64")]
843    #[inline]
844    unsafe fn then_sse(&self, other: &Self) -> Self { unsafe {
845        Self {
846            m: [
847                Self::linear_combine_sse(self.m[0], other),
848                Self::linear_combine_sse(self.m[1], other),
849                Self::linear_combine_sse(self.m[2], other),
850                Self::linear_combine_sse(self.m[3], other),
851            ],
852        }
853    }}
854
855    /// Dual linear combination using AVX instructions on YMM registers.
856    ///
857    /// AUDIT: the rows `b.m[i]` are `[f32; 4]` fields with alignment 4, but
858    /// `_mm256_broadcast_ps` takes a `&__m128` (alignment 16). Forming that
859    /// reference — `&*(ptr as *const __m128)` — from an align-4 field is
860    /// misaligned-reference UB even though the underlying `vbroadcastf128`
861    /// tolerates it. Use `_mm256_loadu2_m128`, which does an *unaligned*
862    /// 128-bit load from a raw `*const f32` and never forms a `&__m128`;
863    /// passing the same row pointer for both lanes reproduces the broadcast
864    /// (`result[127:0] = result[255:128] = row`).
865    ///
866    /// SAFETY: caller must guarantee AVX is available. Each `broadcast_row`
867    /// reads exactly 4 f32 (16 bytes) through `_mm256_loadu2_m128`, an
868    /// *unaligned* load, so the align-4 `[f32; 4]` rows are read in-bounds and
869    /// no `&__m128` (align 16) is ever formed from them.
870    #[cfg(target_arch = "x86_64")]
871    unsafe fn linear_combine_avx8(
872        a01: core::arch::x86_64::__m256,
873        b: &Self,
874    ) -> core::arch::x86_64::__m256 { unsafe {
875        use core::arch::x86_64::{
876            _mm256_add_ps, _mm256_loadu2_m128, _mm256_mul_ps, _mm256_shuffle_ps,
877        };
878
879        // Unaligned broadcast of a row into both 128-bit lanes. Runs inside the
880        // enclosing `unsafe` block, so the intrinsic call needs no inner `unsafe`.
881        let broadcast_row = |row: &[f32; 4]| {
882            let p = row.as_ptr();
883            _mm256_loadu2_m128(p, p)
884        };
885
886        let mut result = _mm256_mul_ps(
887            _mm256_shuffle_ps(a01, a01, 0x00),
888            broadcast_row(&b.m[0]),
889        );
890        result = _mm256_add_ps(
891            result,
892            _mm256_mul_ps(_mm256_shuffle_ps(a01, a01, 0x55), broadcast_row(&b.m[1])),
893        );
894        result = _mm256_add_ps(
895            result,
896            _mm256_mul_ps(_mm256_shuffle_ps(a01, a01, 0xaa), broadcast_row(&b.m[2])),
897        );
898        result = _mm256_add_ps(
899            result,
900            _mm256_mul_ps(_mm256_shuffle_ps(a01, a01, 0xff), broadcast_row(&b.m[3])),
901        );
902        result
903    }}
904
905    /// Multiplies this matrix by `other` using AVX instructions.
906    ///
907    /// SAFETY: caller must guarantee AVX is available. Both `_mm256_loadu_ps`
908    /// reads and `_mm256_storeu_ps` writes are *unaligned* 8-f32 (32-byte)
909    /// accesses. `m` is `[[f32; 4]; 4]`, i.e. 16 contiguous f32 with no padding,
910    /// so `&m[0][0]..` and `&m[2][0]..` each span two full rows in-bounds; the
911    /// raw pointers come from live `self`/`out` locals, so lifetimes are valid.
912    #[cfg(target_arch = "x86_64")]
913    #[inline]
914    unsafe fn then_avx8(&self, other: &Self) -> Self { unsafe {
915        use core::{
916            arch::x86_64::{__m256, _mm256_loadu_ps, _mm256_storeu_ps, _mm256_zeroupper},
917            mem,
918        };
919
920        _mm256_zeroupper();
921
922        let a01: __m256 = _mm256_loadu_ps(&raw const self.m[0][0]);
923        let a23: __m256 = _mm256_loadu_ps(&raw const self.m[2][0]);
924
925        let out01x = Self::linear_combine_avx8(a01, other);
926        let out23x = Self::linear_combine_avx8(a23, other);
927
928        let mut out = Self {
929            m: [self.m[0], self.m[1], self.m[2], self.m[3]],
930        };
931
932        _mm256_storeu_ps(&raw mut out.m[0][0], out01x);
933        _mm256_storeu_ps(&raw mut out.m[2][0], out23x);
934
935        out
936    }}
937
938    /// Creates a rotation matrix around the given axis, adjusted for the coordinate system.
939    #[must_use]
940    #[inline]
941    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
942    fn make_rotation(
943        rotation_origin: (f32, f32),
944        mut degrees: f32,
945        axis_x: f32,
946        axis_y: f32,
947        axis_z: f32,
948        // see documentation for RotationMode
949        rotation_mode: RotationMode,
950    ) -> Self {
951        degrees = match rotation_mode {
952            // CSS rotations are clockwise
953            RotationMode::ForWebRender => -degrees,
954            // hit-testing turns counter-clockwise
955            RotationMode::ForHitTesting => degrees,
956        };
957
958        let (origin_x, origin_y) = rotation_origin;
959        let pre_transform = Self::new_translation(-origin_x, -origin_y, 0.0);
960        let post_transform = Self::new_translation(origin_x, origin_y, 0.0);
961        let theta = 2.0_f32 * core::f32::consts::PI - degrees.to_radians();
962        let rotate_transform =
963            Self::new_rotation(axis_x, axis_y, axis_z, theta);
964
965        pre_transform.then(&rotate_transform).then(&post_transform)
966    }
967}
968
969#[cfg(test)]
970#[allow(clippy::items_after_statements, clippy::redundant_clone, clippy::cast_possible_truncation, clippy::cast_sign_loss, trivial_casts, clippy::borrow_as_ptr, clippy::cast_ptr_alignment, clippy::unused_self, unused_qualifications, unreachable_pub, private_interfaces)] // pedantic lints are noise in unsafe-exercising test code
971mod audit_tests {
972    use super::*;
973
974    fn sample_a() -> ComputedTransform3D {
975        ComputedTransform3D::new(
976            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
977        )
978    }
979    fn sample_b() -> ComputedTransform3D {
980        ComputedTransform3D::new(
981            16.0, 15.0, 14.0, 13.0, 12.0, 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0,
982        )
983    }
984
985    fn approx_eq(a: &ComputedTransform3D, b: &ComputedTransform3D) {
986        for r in 0..4 {
987            for c in 0..4 {
988                assert!(
989                    (a.m[r][c] - b.m[r][c]).abs() < 1e-3,
990                    "mismatch at [{r}][{c}]: {} vs {}",
991                    a.m[r][c],
992                    b.m[r][c]
993                );
994            }
995        }
996    }
997
998    /// Naive row-major 4x4 multiply used as an independent reference for the
999    /// `then` (and hence SIMD) paths. Deliberately avoids `mul_add` so it is a
1000    /// separate implementation from the code under test.
1001    fn naive_then(a: &ComputedTransform3D, b: &ComputedTransform3D) -> ComputedTransform3D {
1002        let mut out = ComputedTransform3D::IDENTITY;
1003        for r in 0..4 {
1004            for c in 0..4 {
1005                let mut acc = 0.0f32;
1006                for k in 0..4 {
1007                    acc += a.m[r][k] * b.m[k][c];
1008                }
1009                out.m[r][c] = acc;
1010            }
1011        }
1012        out
1013    }
1014
1015    // Miri-compatible: exercises only the safe scalar `then` against an
1016    // independent naive reference. Runs everywhere, including under Miri, so the
1017    // scalar anchor that the SIMD paths are compared against is itself checked.
1018    #[test]
1019    fn scalar_matmul_matches_reference() {
1020        let a = sample_a();
1021        let b = sample_b();
1022        approx_eq(&a.then(&b), &naive_then(&a, &b));
1023        // Identity is a left/right unit.
1024        approx_eq(&ComputedTransform3D::IDENTITY.then(&b), &b);
1025        approx_eq(&a.then(&ComputedTransform3D::IDENTITY), &a);
1026    }
1027
1028    // AUDIT: the SSE/AVX matrix-multiply paths must agree with the scalar
1029    // reference. In particular this exercises `linear_combine_avx8`, whose
1030    // unaligned-load fix (`_mm256_loadu2_m128` instead of forming a misaligned
1031    // `&__m128`) must produce identical results. Only runs the SIMD paths when
1032    // the CPU (and OS) actually support the feature.
1033    //
1034    // `#[cfg(not(miri))]`: the AVX/SSE intrinsics cannot execute under Miri, so
1035    // this test is skipped there; a native run covers it.
1036    #[cfg(not(miri))]
1037    #[test]
1038    fn simd_matmul_matches_scalar() {
1039        let a = sample_a();
1040        let b = sample_b();
1041        let scalar = a.then(&b);
1042
1043        #[cfg(target_arch = "x86_64")]
1044        {
1045            if std::is_x86_feature_detected!("sse") {
1046                let sse = unsafe { a.then_sse(&b) };
1047                approx_eq(&scalar, &sse);
1048            }
1049            if std::is_x86_feature_detected!("avx") {
1050                let avx = unsafe { a.then_avx8(&b) };
1051                approx_eq(&scalar, &avx);
1052            }
1053        }
1054
1055        // Always assert the scalar path is self-consistent (identity * b == b).
1056        approx_eq(&ComputedTransform3D::IDENTITY.then(&b), &b);
1057    }
1058
1059    // AUDIT regression test for the misaligned-`&__m128` bug: the AVX path reads
1060    // matrix rows (`[f32; 4]`, alignment 4) that are NOT guaranteed to sit on a
1061    // 16-byte boundary. The earlier code formed a `&__m128` from such a row,
1062    // which is misaligned-reference UB; the current code uses unaligned loads.
1063    // This runs `then_avx8` on the same logical matrix placed at a 16-byte
1064    // aligned address AND at that address + 4 (i.e. 4-mod-16, deliberately not
1065    // 16-aligned) and asserts identical results. A sanitizer/Valgrind run over
1066    // this test would fault on the pre-fix misaligned access.
1067    //
1068    // `#[cfg(not(miri))]`: invokes AVX intrinsics, which Miri cannot execute.
1069    #[cfg(all(target_arch = "x86_64", not(miri)))]
1070    #[test]
1071    fn avx_result_independent_of_row_alignment() {
1072        if !std::is_x86_feature_detected!("avx") {
1073            return;
1074        }
1075
1076        let a = sample_a();
1077        let b = sample_b();
1078        let expected = unsafe { a.then_avx8(&b) };
1079
1080        const N: usize = core::mem::size_of::<ComputedTransform3D>(); // 64, no padding
1081        let mut buf = vec![0u8; N * 2 + 16];
1082        let base = buf.as_mut_ptr();
1083
1084        // SAFETY: `aligned` lands within `buf` (align_offset < 16, then +N),
1085        // `misaligned` = aligned + 4 stays in-bounds (buf has N*2+16 bytes).
1086        // Both are >= 4-byte aligned (base is heap-aligned; +4 preserves that),
1087        // so forming `&ComputedTransform3D` (alignment 4) from them is valid.
1088        unsafe {
1089            let aligned = base.add(base.align_offset(16));
1090            let misaligned = aligned.add(4); // 4 mod 16: not 16-aligned
1091            for off_ptr in [aligned, misaligned] {
1092                core::ptr::copy_nonoverlapping(
1093                    (&raw const a).cast::<u8>(),
1094                    off_ptr,
1095                    N,
1096                );
1097                let a_ref = &*off_ptr.cast::<ComputedTransform3D>();
1098                let got = a_ref.then_avx8(&b);
1099                approx_eq(&expected, &got);
1100            }
1101        }
1102    }
1103}
1104
1105#[cfg(test)]
1106#[allow(
1107    clippy::float_cmp,
1108    clippy::unreadable_literal,
1109    clippy::excessive_precision,
1110    clippy::cast_possible_truncation,
1111    clippy::cast_precision_loss,
1112    clippy::too_many_lines,
1113    clippy::needless_range_loop,
1114    clippy::suboptimal_flops,
1115    unused_qualifications
1116)] // adversarial numeric tests: exact FP comparisons and literal matrices are the point
1117mod autotest_generated {
1118    use azul_css::props::basic::{
1119        AngleValue, FloatValue, PercentageValue, PixelValue, SizeMetric,
1120    };
1121    use azul_css::props::style::{
1122        StyleTransformMatrix2D, StyleTransformMatrix3D, StyleTransformRotate3D,
1123        StyleTransformScale2D, StyleTransformScale3D, StyleTransformSkew2D,
1124        StyleTransformTranslate2D, StyleTransformTranslate3D,
1125    };
1126
1127    use super::*;
1128
1129    // ---------------------------------------------------------------- helpers
1130
1131    /// A matrix whose 16 entries are all `v` — the worst case for any code that
1132    /// assumes a well-formed (invertible / affine) transform.
1133    fn filled(v: f32) -> ComputedTransform3D {
1134        ComputedTransform3D { m: [[v; 4]; 4] }
1135    }
1136
1137    fn assert_mat_approx(a: &ComputedTransform3D, b: &ComputedTransform3D, tol: f32) {
1138        for r in 0..4 {
1139            for c in 0..4 {
1140                assert!(
1141                    (a.m[r][c] - b.m[r][c]).abs() <= tol,
1142                    "mismatch at [{r}][{c}]: {} vs {} (tol {tol})",
1143                    a.m[r][c],
1144                    b.m[r][c]
1145                );
1146            }
1147        }
1148    }
1149
1150    fn all_finite(t: &ComputedTransform3D) -> bool {
1151        t.m.iter().flatten().all(|v| v.is_finite())
1152    }
1153
1154    /// Independent 3x3 determinant (Sarrus) for the reference 4x4 below.
1155    fn det3(m: [[f32; 3]; 3]) -> f32 {
1156        m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
1157            - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
1158            + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
1159    }
1160
1161    /// Independent 4x4 determinant via cofactor expansion along row 0. This is a
1162    /// deliberately different algorithm from the 24-term expansion in
1163    /// `ComputedTransform3D::determinant`, so agreement is a real cross-check.
1164    fn det4_naive(t: &ComputedTransform3D) -> f32 {
1165        let mut sum = 0.0f32;
1166        for col in 0..4 {
1167            let mut minor = [[0.0f32; 3]; 3];
1168            for r in 1..4 {
1169                let mut cc = 0;
1170                for c in 0..4 {
1171                    if c == col {
1172                        continue;
1173                    }
1174                    minor[r - 1][cc] = t.m[r][c];
1175                    cc += 1;
1176                }
1177            }
1178            let sign = if col % 2 == 0 { 1.0 } else { -1.0 };
1179            sum += sign * t.m[0][col] * det3(minor);
1180        }
1181        sum
1182    }
1183
1184    /// `row * B` — the reference for the SSE/AVX linear-combination kernels.
1185    /// (Only reachable from the x86_64 / non-Miri tests below.)
1186    #[allow(dead_code)]
1187    fn naive_row_combine(a: [f32; 4], b: &ComputedTransform3D) -> [f32; 4] {
1188        let mut out = [0.0f32; 4];
1189        for c in 0..4 {
1190            for k in 0..4 {
1191                out[c] += a[k] * b.m[k][c];
1192            }
1193        }
1194        out
1195    }
1196
1197    fn origin_px(x: isize, y: isize) -> StyleTransformOrigin {
1198        StyleTransformOrigin {
1199            x: PixelValue::const_px(x),
1200            y: PixelValue::const_px(y),
1201        }
1202    }
1203
1204    /// Convenience: run a single `StyleTransform` through the private builder
1205    /// with a zero origin (so rotations are not wrapped in translations).
1206    fn build(t: &StyleTransform, px: f32, py: f32) -> ComputedTransform3D {
1207        ComputedTransform3D::from_style_transform(t, &origin_px(0, 0), px, py, RotationMode::ForHitTesting)
1208    }
1209
1210    // ------------------------------------------------------ constructors: new
1211
1212    #[test]
1213    fn new_stores_all_16_elements_row_major() {
1214        let t = ComputedTransform3D::new(
1215            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1216        );
1217        for r in 0..4 {
1218            for c in 0..4 {
1219                let expected = (r * 4 + c + 1) as f32;
1220                assert_eq!(t.m[r][c], expected, "row-major slot [{r}][{c}]");
1221            }
1222        }
1223    }
1224
1225    #[test]
1226    fn new_preserves_extreme_values_verbatim() {
1227        let t = ComputedTransform3D::new(
1228            f32::NAN,
1229            f32::INFINITY,
1230            f32::NEG_INFINITY,
1231            f32::MAX,
1232            f32::MIN,
1233            f32::MIN_POSITIVE,
1234            -0.0,
1235            0.0,
1236            f32::EPSILON,
1237            -f32::EPSILON,
1238            1e-45, // subnormal
1239            -1e-45,
1240            f32::MAX,
1241            f32::MIN,
1242            f32::INFINITY,
1243            f32::NEG_INFINITY,
1244        );
1245        // The constructor must not sanitize, clamp or panic on any of these.
1246        assert!(t.m[0][0].is_nan());
1247        assert!(t.m[0][1].is_infinite() && t.m[0][1].is_sign_positive());
1248        assert!(t.m[0][2].is_infinite() && t.m[0][2].is_sign_negative());
1249        assert_eq!(t.m[0][3], f32::MAX);
1250        assert_eq!(t.m[1][0], f32::MIN);
1251        assert_eq!(t.m[1][1], f32::MIN_POSITIVE);
1252        // -0.0 must survive as -0.0 (it compares == 0.0, so check the sign bit).
1253        assert!(t.m[1][2].is_sign_negative());
1254        assert!(t.m[1][3].is_sign_positive());
1255        assert_eq!(t.m[2][0], f32::EPSILON);
1256        assert!(t.m[3][2].is_infinite());
1257    }
1258
1259    #[test]
1260    fn new_2d_matches_css_matrix_layout() {
1261        // matrix(a, b, c, d, tx, ty) => [[a,b,0,0],[c,d,0,0],[0,0,1,0],[tx,ty,0,1]]
1262        let t = ComputedTransform3D::new_2d(2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
1263        assert_eq!(t.m[0], [2.0, 3.0, 0.0, 0.0]);
1264        assert_eq!(t.m[1], [4.0, 5.0, 0.0, 0.0]);
1265        assert_eq!(t.m[2], [0.0, 0.0, 1.0, 0.0]); // Z untouched
1266        assert_eq!(t.m[3], [6.0, 7.0, 0.0, 1.0]);
1267    }
1268
1269    #[test]
1270    fn new_2d_with_extremes_keeps_z_row_intact() {
1271        let t = ComputedTransform3D::new_2d(
1272            f32::NAN,
1273            f32::INFINITY,
1274            f32::MAX,
1275            f32::MIN,
1276            f32::NEG_INFINITY,
1277            -0.0,
1278        );
1279        assert!(t.m[0][0].is_nan());
1280        // The constant Z row / W column must not be corrupted by extreme args.
1281        assert_eq!(t.m[2], [0.0, 0.0, 1.0, 0.0]);
1282        assert_eq!(t.m[3][3], 1.0);
1283    }
1284
1285    // -------------------------------------------- constructors: scale / translate
1286
1287    #[test]
1288    fn new_scale_places_factors_on_the_diagonal() {
1289        let t = ComputedTransform3D::new_scale(2.0, -3.0, 0.5);
1290        assert_eq!(t.m[0][0], 2.0);
1291        assert_eq!(t.m[1][1], -3.0);
1292        assert_eq!(t.m[2][2], 0.5);
1293        assert_eq!(t.m[3][3], 1.0);
1294        // Everything off the diagonal stays zero.
1295        for r in 0..4 {
1296            for c in 0..4 {
1297                if r != c {
1298                    assert_eq!(t.m[r][c], 0.0, "off-diagonal [{r}][{c}]");
1299                }
1300            }
1301        }
1302    }
1303
1304    #[test]
1305    fn new_scale_zero_is_singular_and_inverse_falls_back_to_identity() {
1306        let z = ComputedTransform3D::new_scale(0.0, 0.0, 0.0);
1307        assert_eq!(z.determinant(), 0.0);
1308        // Documented: a singular matrix inverts to the identity rather than
1309        // producing inf/NaN or panicking.
1310        assert_eq!(z.inverse(), ComputedTransform3D::IDENTITY);
1311    }
1312
1313    #[test]
1314    fn new_scale_extremes_do_not_panic() {
1315        for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
1316            let t = ComputedTransform3D::new_scale(v, v, v);
1317            assert_eq!(t.m[3][3], 1.0);
1318            assert_eq!(t.m[0][1], 0.0);
1319        }
1320        let nan = ComputedTransform3D::new_scale(f32::NAN, 1.0, 1.0);
1321        assert!(nan.m[0][0].is_nan());
1322        assert!(nan.determinant().is_nan());
1323    }
1324
1325    #[test]
1326    fn new_translation_places_offsets_in_last_row() {
1327        let t = ComputedTransform3D::new_translation(10.0, -20.0, 30.0);
1328        assert_eq!(t.m[3], [10.0, -20.0, 30.0, 1.0]);
1329        // The upper-left 3x3 stays the identity.
1330        assert_eq!(t.m[0], [1.0, 0.0, 0.0, 0.0]);
1331        assert_eq!(t.m[1], [0.0, 1.0, 0.0, 0.0]);
1332        assert_eq!(t.m[2], [0.0, 0.0, 1.0, 0.0]);
1333    }
1334
1335    #[test]
1336    fn new_translation_is_always_invertible_even_at_f32_max() {
1337        let t = ComputedTransform3D::new_translation(f32::MAX, f32::MIN, f32::MAX);
1338        // determinant of a translation is 1 regardless of how big the offsets are
1339        assert_eq!(t.determinant(), 1.0);
1340        let inv = t.inverse();
1341        assert_eq!(inv.m[3][0], -f32::MAX);
1342        assert_eq!(inv.m[3][1], f32::MAX); // -f32::MIN
1343    }
1344
1345    #[test]
1346    fn new_translation_nan_does_not_poison_the_linear_part() {
1347        let t = ComputedTransform3D::new_translation(f32::NAN, f32::INFINITY, 0.0);
1348        assert!(t.m[3][0].is_nan());
1349        assert!(t.m[3][1].is_infinite());
1350        assert_eq!(t.m[0][0], 1.0);
1351        // determinant only sees the 1.0s and 0.0s of the linear part... except the
1352        // 24-term expansion also multiplies through the translation row, so a NaN
1353        // offset does reach it. Assert the *actual* (defined) behaviour: NaN in,
1354        // NaN out — no panic.
1355        assert!(t.determinant().is_nan() || t.determinant() == 1.0);
1356    }
1357
1358    // ------------------------------------------- constructors: perspective / skew
1359
1360    #[test]
1361    fn new_perspective_finite_distance() {
1362        let t = ComputedTransform3D::new_perspective(100.0);
1363        assert!((t.m[2][3] - (-0.01)).abs() < 1e-6);
1364        assert_eq!(t.m[0][0], 1.0);
1365        assert_eq!(t.m[3][3], 1.0);
1366    }
1367
1368    #[test]
1369    fn new_perspective_zero_distance_divides_by_zero() {
1370        // -1.0 / 0.0 is -inf in IEEE-754: no panic, but the matrix is unusable.
1371        // This is the documented (if hazardous) behaviour of `perspective(0)`.
1372        let t = ComputedTransform3D::new_perspective(0.0);
1373        assert!(t.m[2][3].is_infinite() && t.m[2][3].is_sign_negative());
1374        assert!(!all_finite(&t));
1375    }
1376
1377    #[test]
1378    fn new_perspective_extreme_distances_do_not_panic() {
1379        let nan = ComputedTransform3D::new_perspective(f32::NAN);
1380        assert!(nan.m[2][3].is_nan());
1381
1382        let inf = ComputedTransform3D::new_perspective(f32::INFINITY);
1383        assert_eq!(inf.m[2][3], -0.0); // -1/inf == -0.0 => equals the identity
1384        assert!(all_finite(&inf));
1385
1386        // -1 / (smallest subnormal) overflows f32 => -inf, still no panic.
1387        let tiny = ComputedTransform3D::new_perspective(1e-45);
1388        assert!(tiny.m[2][3].is_infinite() && tiny.m[2][3].is_sign_negative());
1389    }
1390
1391    #[test]
1392    fn new_skew_45_degrees_is_unit_shear() {
1393        // new_skew(alpha, beta): m[1][0] = tan(alpha), m[0][1] = tan(beta)
1394        let t = ComputedTransform3D::new_skew(45.0, 0.0);
1395        assert!((t.m[1][0] - 1.0).abs() < 1e-5, "tan(45deg) ~= 1, got {}", t.m[1][0]);
1396        assert_eq!(t.m[0][1], 0.0);
1397        assert_eq!(t.m[0][0], 1.0);
1398        assert_eq!(t.m[1][1], 1.0);
1399        // A unit shear preserves area.
1400        assert!((t.determinant() - 1.0).abs() < 1e-4);
1401    }
1402
1403    #[test]
1404    fn new_skew_90_degrees_stays_finite() {
1405        // tan(pi/2) is not representable; f32's rounded pi/2 makes tan() a huge
1406        // (but finite) number rather than inf. Assert it does not panic and that
1407        // nothing becomes NaN/inf.
1408        let t = ComputedTransform3D::new_skew(90.0, 90.0);
1409        assert!(all_finite(&t), "skew(90deg) produced a non-finite entry: {t:?}");
1410        assert!(t.m[1][0].abs() > 1e6, "expected a huge shear, got {}", t.m[1][0]);
1411    }
1412
1413    #[test]
1414    fn new_skew_nan_and_infinite_angles_do_not_panic() {
1415        let nan = ComputedTransform3D::new_skew(f32::NAN, 0.0);
1416        assert!(nan.m[1][0].is_nan());
1417        assert_eq!(nan.m[3][3], 1.0);
1418
1419        // tan(inf) is NaN, not a panic.
1420        let inf = ComputedTransform3D::new_skew(f32::INFINITY, f32::NEG_INFINITY);
1421        assert!(inf.m[1][0].is_nan());
1422        assert!(inf.m[0][1].is_nan());
1423    }
1424
1425    // --------------------------------------------------- constructors: rotation
1426
1427    #[test]
1428    fn new_rotation_zero_angle_is_identity() {
1429        let t = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, 0.0);
1430        assert_mat_approx(&t, &ComputedTransform3D::IDENTITY, 1e-6);
1431    }
1432
1433    #[test]
1434    fn new_rotation_quarter_turn_about_z() {
1435        let t = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, core::f32::consts::FRAC_PI_2);
1436        // sq = sin^2(pi/4) = 0.5, sc = sin*cos(pi/4) = 0.5
1437        assert!((t.m[0][0] - 0.0).abs() < 1e-6);
1438        assert!((t.m[0][1] - 1.0).abs() < 1e-6);
1439        assert!((t.m[1][0] - -1.0).abs() < 1e-6);
1440        assert!((t.m[1][1] - 0.0).abs() < 1e-6);
1441        assert_eq!(t.m[2][2], 1.0);
1442    }
1443
1444    #[test]
1445    fn new_rotation_is_orthonormal_and_det_one() {
1446        // Normalized axis, as the doc requires.
1447        let (x, y, z) = (0.267_261_24, 0.534_522_5, 0.801_783_7); // (1,2,3)/|(1,2,3)|
1448        let t = ComputedTransform3D::new_rotation(x, y, z, 0.7);
1449        assert!((t.determinant() - 1.0).abs() < 1e-4, "det = {}", t.determinant());
1450        for r in 0..3 {
1451            let len_sq =
1452                t.m[r][0] * t.m[r][0] + t.m[r][1] * t.m[r][1] + t.m[r][2] * t.m[r][2];
1453            assert!((len_sq - 1.0).abs() < 1e-4, "row {r} is not unit length: {len_sq}");
1454        }
1455        // For a pure rotation, inverse == transpose.
1456        assert_mat_approx(&t.inverse(), &t.get_column_major(), 1e-4);
1457    }
1458
1459    #[test]
1460    fn new_rotation_degenerate_zero_axis_yields_identity() {
1461        // The doc says the axis "must be normalized"; a zero axis is the classic
1462        // caller mistake. It must not panic or produce NaN — it degenerates to
1463        // the identity (every term is multiplied by an axis component).
1464        let t = ComputedTransform3D::new_rotation(0.0, 0.0, 0.0, 1.234);
1465        assert_mat_approx(&t, &ComputedTransform3D::IDENTITY, 1e-6);
1466    }
1467
1468    #[test]
1469    fn new_rotation_nan_and_infinite_theta_do_not_panic() {
1470        let nan = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, f32::NAN);
1471        assert!(nan.m[0][0].is_nan());
1472        assert_eq!(nan.m[3][3], 1.0); // the constant W row survives
1473
1474        // sin(inf) / cos(inf) are NaN in IEEE-754 — again, no panic.
1475        let inf = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, f32::INFINITY);
1476        assert!(inf.m[0][0].is_nan());
1477    }
1478
1479    #[test]
1480    fn new_rotation_huge_theta_stays_bounded() {
1481        // A rotation matrix must stay in [-1, 1] no matter how absurd the angle.
1482        let t = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, 1e9);
1483        assert!(all_finite(&t));
1484        for r in 0..3 {
1485            for c in 0..3 {
1486                assert!(t.m[r][c].abs() <= 1.001, "entry [{r}][{c}] = {} escaped [-1,1]", t.m[r][c]);
1487            }
1488        }
1489    }
1490
1491    // ------------------------------------------------------------- determinant
1492
1493    #[test]
1494    fn determinant_of_identity_is_one() {
1495        assert_eq!(ComputedTransform3D::IDENTITY.determinant(), 1.0);
1496    }
1497
1498    #[test]
1499    fn determinant_of_diagonal_is_product() {
1500        let t = ComputedTransform3D::new(
1501            2.0, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0,
1502        );
1503        assert_eq!(t.determinant(), 120.0);
1504    }
1505
1506    #[test]
1507    fn determinant_matches_independent_cofactor_expansion() {
1508        let t = ComputedTransform3D::new(
1509            3.0, 1.0, 0.0, 2.0, 0.0, 2.0, 1.0, 1.0, 1.0, 0.0, 4.0, 0.0, 2.0, 1.0, 1.0, 3.0,
1510        );
1511        let got = t.determinant();
1512        let want = det4_naive(&t);
1513        assert!((got - want).abs() < 1e-3, "determinant() = {got}, cofactor ref = {want}");
1514    }
1515
1516    #[test]
1517    fn determinant_of_singular_matrices_is_zero() {
1518        // All-zero matrix.
1519        assert_eq!(filled(0.0).determinant(), 0.0);
1520        // Two identical rows => rank-deficient. Small integers keep the products
1521        // exact, so the cancellation is exact too.
1522        let dup = ComputedTransform3D::new(
1523            1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0, 0.0, 1.0, 0.0, 2.0, 4.0, 3.0, 2.0, 1.0,
1524        );
1525        assert!(dup.determinant().abs() < 1e-4, "det = {}", dup.determinant());
1526        // A matrix where every entry is the same is also singular.
1527        assert!(filled(7.0).determinant().abs() < 1e-2);
1528    }
1529
1530    #[test]
1531    fn determinant_overflows_to_infinity_rather_than_wrapping() {
1532        // diag(1e20)^4 = 1e80, far beyond f32::MAX (~3.4e38): saturates to +inf.
1533        let big = ComputedTransform3D::new_scale(1e20, 1e20, 1e20);
1534        let mut big = big;
1535        big.m[3][3] = 1e20;
1536        let det = big.determinant();
1537        assert!(det.is_infinite() && det.is_sign_positive(), "det = {det}");
1538    }
1539
1540    #[test]
1541    fn determinant_of_nan_matrix_is_nan_not_a_panic() {
1542        assert!(filled(f32::NAN).determinant().is_nan());
1543    }
1544
1545    // ----------------------------------------------------------------- inverse
1546
1547    #[test]
1548    fn inverse_of_identity_is_identity() {
1549        assert_mat_approx(
1550            &ComputedTransform3D::IDENTITY.inverse(),
1551            &ComputedTransform3D::IDENTITY,
1552            1e-6,
1553        );
1554    }
1555
1556    #[test]
1557    fn inverse_round_trips_to_identity() {
1558        let t = ComputedTransform3D::new_translation(10.0, 20.0, 30.0)
1559            .then(&ComputedTransform3D::new_scale(2.0, 4.0, 8.0));
1560        assert_mat_approx(&t.then(&t.inverse()), &ComputedTransform3D::IDENTITY, 1e-4);
1561        assert_mat_approx(&t.inverse().then(&t), &ComputedTransform3D::IDENTITY, 1e-4);
1562    }
1563
1564    #[test]
1565    fn inverse_of_singular_matrix_returns_identity() {
1566        assert_eq!(filled(0.0).inverse(), ComputedTransform3D::IDENTITY);
1567        assert_eq!(
1568            ComputedTransform3D::new_scale(1.0, 1.0, 0.0).inverse(),
1569            ComputedTransform3D::IDENTITY
1570        );
1571    }
1572
1573    #[test]
1574    fn inverse_treats_near_singular_as_singular() {
1575        // PRECISION HAZARD, asserted as the documented contract: the guard is
1576        // `det.abs() < f32::EPSILON` (~1.19e-7), so a *perfectly invertible*
1577        // uniform scale of 1e-3 (det = 1e-9) is rejected and the identity is
1578        // returned instead of the true inverse (a 1000x up-scale).
1579        let tiny = ComputedTransform3D::new_scale(1e-3, 1e-3, 1e-3);
1580        let det = tiny.determinant();
1581        assert!(det > 0.0 && det < f32::EPSILON, "det = {det} (must be a nonzero sub-EPSILON)");
1582        assert_eq!(tiny.inverse(), ComputedTransform3D::IDENTITY);
1583    }
1584
1585    #[test]
1586    fn inverse_of_nan_matrix_does_not_panic() {
1587        // det is NaN, and `NaN.abs() < EPSILON` is false, so the singular guard
1588        // does NOT catch it: the algorithm runs and yields an all-NaN matrix.
1589        let inv = filled(f32::NAN).inverse();
1590        assert!(inv.m.iter().flatten().all(|v| v.is_nan()));
1591    }
1592
1593    #[test]
1594    fn inverse_of_overflowing_matrix_yields_nan_not_a_panic() {
1595        // det = +inf => scale factor 1/inf = 0.0 => cofactor(inf) * 0.0 = NaN.
1596        let mut big = ComputedTransform3D::new_scale(1e20, 1e20, 1e20);
1597        big.m[3][3] = 1e20;
1598        let inv = big.inverse();
1599        assert!(inv.m[0][0].is_nan(), "expected NaN from inf * 0.0, got {}", inv.m[0][0]);
1600    }
1601
1602    // ------------------------------------------------------- multiply_scalar
1603
1604    #[test]
1605    fn multiply_scalar_by_zero_zeroes_every_entry() {
1606        let t = ComputedTransform3D::IDENTITY.multiply_scalar(0.0);
1607        assert!(t.m.iter().flatten().all(|v| *v == 0.0));
1608    }
1609
1610    #[test]
1611    fn multiply_scalar_is_sign_and_magnitude_exact() {
1612        let t = ComputedTransform3D::new_scale(2.0, 3.0, 4.0).multiply_scalar(-1.0);
1613        assert_eq!(t.m[0][0], -2.0);
1614        assert_eq!(t.m[1][1], -3.0);
1615        assert_eq!(t.m[2][2], -4.0);
1616        assert_eq!(t.m[3][3], -1.0);
1617
1618        let m = ComputedTransform3D::IDENTITY.multiply_scalar(f32::MAX);
1619        assert_eq!(m.m[0][0], f32::MAX);
1620        assert_eq!(m.m[0][1], 0.0);
1621    }
1622
1623    #[test]
1624    fn multiply_scalar_overflow_saturates_to_infinity() {
1625        let t = filled(1e30).multiply_scalar(1e30);
1626        assert!(t.m.iter().flatten().all(|v| v.is_infinite() && v.is_sign_positive()));
1627    }
1628
1629    #[test]
1630    fn multiply_scalar_by_infinity_poisons_zero_entries_with_nan() {
1631        // IEEE-754: 0.0 * inf == NaN. Scaling the IDENTITY by inf therefore does
1632        // NOT give an "infinitely scaled identity" — every off-diagonal 0 becomes
1633        // NaN. Asserted so a future refactor cannot silently change it.
1634        let t = ComputedTransform3D::IDENTITY.multiply_scalar(f32::INFINITY);
1635        assert!(t.m[0][0].is_infinite());
1636        assert!(t.m[0][1].is_nan(), "0.0 * inf should be NaN, got {}", t.m[0][1]);
1637    }
1638
1639    #[test]
1640    fn multiply_scalar_by_nan_makes_everything_nan() {
1641        let t = ComputedTransform3D::new_scale(2.0, 3.0, 4.0).multiply_scalar(f32::NAN);
1642        assert!(t.m.iter().flatten().all(|v| v.is_nan()));
1643    }
1644
1645    // ------------------------------------------------------ get_column_major
1646
1647    #[test]
1648    fn get_column_major_transposes() {
1649        let t = ComputedTransform3D::new(
1650            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1651        );
1652        let c = t.get_column_major();
1653        for r in 0..4 {
1654            for col in 0..4 {
1655                assert_eq!(c.m[r][col], t.m[col][r], "transpose slot [{r}][{col}]");
1656            }
1657        }
1658    }
1659
1660    #[test]
1661    fn get_column_major_is_an_involution() {
1662        let t = ComputedTransform3D::new_translation(3.0, -4.0, 5.0)
1663            .then(&ComputedTransform3D::new_scale(2.0, 2.0, 2.0));
1664        assert_eq!(t.get_column_major().get_column_major(), t);
1665        // The identity is its own transpose.
1666        assert_eq!(
1667            ComputedTransform3D::IDENTITY.get_column_major(),
1668            ComputedTransform3D::IDENTITY
1669        );
1670    }
1671
1672    #[test]
1673    fn get_column_major_moves_translation_into_the_last_column() {
1674        let t = ComputedTransform3D::new_translation(7.0, 8.0, 9.0).get_column_major();
1675        assert_eq!(t.m[0][3], 7.0);
1676        assert_eq!(t.m[1][3], 8.0);
1677        assert_eq!(t.m[2][3], 9.0);
1678        assert_eq!(t.m[3], [0.0, 0.0, 0.0, 1.0]);
1679    }
1680
1681    #[test]
1682    fn get_column_major_of_nan_matrix_does_not_panic() {
1683        let t = filled(f32::NAN).get_column_major();
1684        assert!(t.m.iter().flatten().all(|v| v.is_nan()));
1685    }
1686
1687    // ----------------------------------------------------- transform_point2d
1688
1689    #[test]
1690    fn transform_point2d_identity_is_the_point_itself() {
1691        let p = LogicalPosition::new(3.0, -4.0);
1692        let out = ComputedTransform3D::IDENTITY.transform_point2d(p).unwrap();
1693        assert_eq!(out.x, 3.0);
1694        assert_eq!(out.y, -4.0);
1695
1696        let zero = ComputedTransform3D::IDENTITY
1697            .transform_point2d(LogicalPosition::zero())
1698            .unwrap();
1699        assert_eq!((zero.x, zero.y), (0.0, 0.0));
1700    }
1701
1702    #[test]
1703    fn transform_point2d_applies_translation_and_scale() {
1704        let t = ComputedTransform3D::new_translation(10.0, 20.0, 0.0);
1705        let out = t.transform_point2d(LogicalPosition::new(1.0, 2.0)).unwrap();
1706        assert_eq!((out.x, out.y), (11.0, 22.0));
1707
1708        let s = ComputedTransform3D::new_scale(2.0, -3.0, 1.0);
1709        let out = s.transform_point2d(LogicalPosition::new(1.5, 2.0)).unwrap();
1710        assert_eq!((out.x, out.y), (3.0, -6.0));
1711    }
1712
1713    #[test]
1714    fn transform_point2d_negative_w_returns_none() {
1715        let mut t = ComputedTransform3D::IDENTITY;
1716        t.m[3][3] = -1.0; // w = -1
1717        assert!(t.transform_point2d(LogicalPosition::new(1.0, 1.0)).is_none());
1718
1719        // w driven negative by the point itself (perspective-style m[0][3]).
1720        let mut p = ComputedTransform3D::IDENTITY;
1721        p.m[0][3] = -1.0; // w = 1 - p.x
1722        assert!(p.transform_point2d(LogicalPosition::new(2.0, 0.0)).is_none());
1723        assert!(p.transform_point2d(LogicalPosition::new(0.5, 0.0)).is_some());
1724    }
1725
1726    #[test]
1727    fn transform_point2d_zero_w_divides_by_zero_instead_of_returning_none() {
1728        // BOUNDARY: the guard is `!w.is_sign_positive()`, and (+0.0) IS
1729        // sign-positive — so a fully degenerate w == +0.0 slips through and the
1730        // function divides by zero, returning Some(inf, inf) rather than None.
1731        // Asserted as-is (no panic, defined IEEE result); flagged in the report.
1732        let mut t = ComputedTransform3D::IDENTITY;
1733        t.m[3][3] = 0.0;
1734        let out = t.transform_point2d(LogicalPosition::new(1.0, 1.0));
1735        let out = out.expect("w == +0.0 is treated as a valid positive w");
1736        assert!(out.x.is_infinite(), "expected 1.0/0.0 = inf, got {}", out.x);
1737        assert!(out.y.is_infinite());
1738
1739        // ... whereas a w that comes out as -0.0 IS rejected, so the sign of a
1740        // zero w decides between Some(inf) and None. (The whole w column must be
1741        // -0.0: fma(1.0, +0.0, -0.0) would round back to +0.0.)
1742        let mut neg = ComputedTransform3D::IDENTITY;
1743        neg.m[0][3] = -0.0;
1744        neg.m[1][3] = -0.0;
1745        neg.m[3][3] = -0.0;
1746        assert!(neg.transform_point2d(LogicalPosition::new(1.0, 1.0)).is_none());
1747    }
1748
1749    #[test]
1750    fn transform_point2d_nan_matrix_does_not_panic() {
1751        let out = filled(f32::NAN).transform_point2d(LogicalPosition::new(1.0, 1.0));
1752        // w is NaN; whichever branch the sign bit lands on, neither may panic.
1753        if let Some(p) = out {
1754            assert!(p.x.is_nan() && p.y.is_nan());
1755        }
1756    }
1757
1758    #[test]
1759    fn transform_point2d_nan_point_does_not_panic() {
1760        let out = ComputedTransform3D::IDENTITY
1761            .transform_point2d(LogicalPosition::new(f32::NAN, f32::NAN));
1762        // w = NaN*0 + (NaN*0 + 1) = NaN => no panic either way.
1763        if let Some(p) = out {
1764            assert!(p.x.is_nan());
1765        }
1766    }
1767
1768    #[test]
1769    fn transform_point2d_extreme_coordinates_saturate() {
1770        let t = ComputedTransform3D::new_translation(10.0, 10.0, 0.0);
1771        let out = t
1772            .transform_point2d(LogicalPosition::new(f32::MAX, f32::MIN))
1773            .unwrap();
1774        assert_eq!(out.x, f32::MAX); // MAX + 10 rounds back to MAX
1775        assert_eq!(out.y, f32::MIN);
1776
1777        // A scale big enough to overflow saturates to inf, it does not wrap.
1778        let s = ComputedTransform3D::new_scale(1e30, 1e30, 1.0);
1779        let out = s
1780            .transform_point2d(LogicalPosition::new(1e30, 1e30))
1781            .unwrap();
1782        assert!(out.x.is_infinite() && out.x.is_sign_positive());
1783    }
1784
1785    // --------------------------------------------------------- scale_for_dpi
1786
1787    #[test]
1788    fn scale_for_dpi_touches_only_the_translation_row() {
1789        let mut t = ComputedTransform3D::new(
1790            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1791        );
1792        let before = t;
1793        t.scale_for_dpi(3.0);
1794        assert_eq!(t.m[0], before.m[0]);
1795        assert_eq!(t.m[1], before.m[1]);
1796        assert_eq!(t.m[2], before.m[2]);
1797        assert_eq!(t.m[3][0], 39.0);
1798        assert_eq!(t.m[3][1], 42.0);
1799        assert_eq!(t.m[3][2], 45.0);
1800        assert_eq!(t.m[3][3], 16.0, "m44 must NOT be scaled");
1801    }
1802
1803    #[test]
1804    fn scale_for_dpi_zero_and_negative() {
1805        let mut t = ComputedTransform3D::new_translation(10.0, 20.0, 30.0);
1806        t.scale_for_dpi(0.0);
1807        assert_eq!(t.m[3], [0.0, 0.0, 0.0, 1.0]);
1808
1809        let mut n = ComputedTransform3D::new_translation(10.0, -20.0, 30.0);
1810        n.scale_for_dpi(-2.0);
1811        assert_eq!(n.m[3], [-20.0, 40.0, -60.0, 1.0]);
1812    }
1813
1814    #[test]
1815    fn scale_for_dpi_is_exactly_reversible_for_powers_of_two() {
1816        let original = ComputedTransform3D::new_translation(13.25, -7.5, 0.125);
1817        let mut t = original;
1818        t.scale_for_dpi(2.0);
1819        t.scale_for_dpi(0.5);
1820        assert_eq!(t, original);
1821    }
1822
1823    #[test]
1824    fn scale_for_dpi_overflow_saturates_to_infinity() {
1825        let mut t = ComputedTransform3D::new_translation(1e38, -1e38, 1e38);
1826        t.scale_for_dpi(1e5);
1827        assert!(t.m[3][0].is_infinite() && t.m[3][0].is_sign_positive());
1828        assert!(t.m[3][1].is_infinite() && t.m[3][1].is_sign_negative());
1829        assert_eq!(t.m[3][3], 1.0);
1830    }
1831
1832    #[test]
1833    fn scale_for_dpi_by_infinity_poisons_a_zero_translation() {
1834        // IEEE-754: 0.0 * inf == NaN. A DPI factor of inf turns the *identity's*
1835        // zero translation into NaN — assert the defined result, no panic.
1836        let mut t = ComputedTransform3D::IDENTITY;
1837        t.scale_for_dpi(f32::INFINITY);
1838        assert!(t.m[3][0].is_nan());
1839        assert_eq!(t.m[0][0], 1.0, "the linear part must stay untouched");
1840
1841        let mut n = ComputedTransform3D::new_translation(1.0, 2.0, 3.0);
1842        n.scale_for_dpi(f32::INFINITY);
1843        assert!(n.m[3][0].is_infinite());
1844    }
1845
1846    #[test]
1847    fn scale_for_dpi_by_nan_does_not_panic() {
1848        let mut t = ComputedTransform3D::new_translation(1.0, 2.0, 3.0);
1849        t.scale_for_dpi(f32::NAN);
1850        assert!(t.m[3][0].is_nan() && t.m[3][1].is_nan() && t.m[3][2].is_nan());
1851        assert_eq!(t.m[3][3], 1.0);
1852        assert_eq!(t.m[0][0], 1.0);
1853    }
1854
1855    // -------------------------------------------------------------------- then
1856
1857    #[test]
1858    fn then_has_identity_as_a_two_sided_unit() {
1859        let a = ComputedTransform3D::new(
1860            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1861        );
1862        assert_mat_approx(&a.then(&ComputedTransform3D::IDENTITY), &a, 1e-4);
1863        assert_mat_approx(&ComputedTransform3D::IDENTITY.then(&a), &a, 1e-4);
1864    }
1865
1866    #[test]
1867    fn then_composes_translations_additively_and_scales_multiplicatively() {
1868        let t = ComputedTransform3D::new_translation(1.0, 2.0, 3.0)
1869            .then(&ComputedTransform3D::new_translation(10.0, 20.0, 30.0));
1870        assert_eq!(t.m[3], [11.0, 22.0, 33.0, 1.0]);
1871
1872        let s = ComputedTransform3D::new_scale(2.0, 3.0, 4.0)
1873            .then(&ComputedTransform3D::new_scale(5.0, 7.0, 11.0));
1874        assert_eq!(s.m[0][0], 10.0);
1875        assert_eq!(s.m[1][1], 21.0);
1876        assert_eq!(s.m[2][2], 44.0);
1877    }
1878
1879    #[test]
1880    fn then_is_associative() {
1881        let a = ComputedTransform3D::new(
1882            1.0, 0.5, 0.0, 0.0, -0.5, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 2.0, 3.0, 0.0, 1.0,
1883        );
1884        let b = ComputedTransform3D::new_scale(2.0, 0.5, 1.0);
1885        let c = ComputedTransform3D::new_translation(-1.0, 4.0, 0.0);
1886        assert_mat_approx(&a.then(&b).then(&c), &a.then(&b.then(&c)), 1e-2);
1887    }
1888
1889    #[test]
1890    fn then_with_extreme_matrices_does_not_panic() {
1891        let big = filled(1e30).then(&filled(1e30));
1892        assert!(big.m[0][0].is_infinite(), "expected overflow to inf, got {}", big.m[0][0]);
1893
1894        let nan = filled(f32::NAN).then(&ComputedTransform3D::IDENTITY);
1895        assert!(nan.m.iter().flatten().all(|v| v.is_nan()));
1896
1897        // inf * 0 inside the dot product => NaN, not a panic.
1898        let mixed = filled(f32::INFINITY).then(&ComputedTransform3D::IDENTITY);
1899        assert!(mixed.m[0][0].is_infinite() || mixed.m[0][0].is_nan());
1900    }
1901
1902    // ------------------------------------------------ SIMD paths (x86_64 only)
1903
1904    // `not(miri)`: Miri cannot execute SSE/AVX intrinsics.
1905    #[cfg(all(target_arch = "x86_64", not(miri)))]
1906    #[test]
1907    fn linear_combine_sse_matches_naive_row_combine() {
1908        if !std::is_x86_feature_detected!("sse") {
1909            return;
1910        }
1911        let b = ComputedTransform3D::new(
1912            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1913        );
1914        for a in [
1915            [1.0f32, 2.0, 3.0, 4.0],
1916            [0.0, 0.0, 0.0, 0.0],
1917            [-1.5, 0.25, 1e6, -1e6],
1918        ] {
1919            // SAFETY: SSE availability was just checked at runtime.
1920            let got = unsafe { ComputedTransform3D::linear_combine_sse(a, &b) };
1921            let want = naive_row_combine(a, &b);
1922            for c in 0..4 {
1923                let tol = 1e-3 * want[c].abs().max(1.0);
1924                assert!((got[c] - want[c]).abs() <= tol, "lane {c}: {} vs {}", got[c], want[c]);
1925            }
1926        }
1927    }
1928
1929    #[cfg(all(target_arch = "x86_64", not(miri)))]
1930    #[test]
1931    fn linear_combine_sse_propagates_nan_per_lane() {
1932        if !std::is_x86_feature_detected!("sse") {
1933            return;
1934        }
1935        // SAFETY: SSE availability was just checked at runtime.
1936        let got = unsafe {
1937            ComputedTransform3D::linear_combine_sse(
1938                [f32::NAN; 4],
1939                &ComputedTransform3D::IDENTITY,
1940            )
1941        };
1942        assert!(got.iter().all(|v| v.is_nan()), "NaN must survive the SIMD path: {got:?}");
1943    }
1944
1945    #[cfg(all(target_arch = "x86_64", not(miri)))]
1946    #[test]
1947    fn then_sse_and_then_avx8_agree_with_scalar_then() {
1948        let a = ComputedTransform3D::new(
1949            1.0, 0.5, -2.0, 0.0, 3.0, 1.0, 0.0, 0.25, 0.0, -1.0, 4.0, 0.0, 5.0, 6.0, 7.0, 1.0,
1950        );
1951        let b = ComputedTransform3D::new(
1952            2.0, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, -4.0, 2.0, 0.5, 1.0,
1953        );
1954        let scalar = a.then(&b);
1955
1956        if std::is_x86_feature_detected!("sse") {
1957            // SAFETY: SSE availability was just checked at runtime.
1958            let sse = unsafe { a.then_sse(&b) };
1959            assert_mat_approx(&scalar, &sse, 1e-3);
1960            // Identity is still a unit through the SIMD path.
1961            // SAFETY: same runtime check as above.
1962            let unit = unsafe { ComputedTransform3D::IDENTITY.then_sse(&b) };
1963            assert_mat_approx(&unit, &b, 1e-4);
1964        }
1965        if std::is_x86_feature_detected!("avx") {
1966            // SAFETY: AVX availability was just checked at runtime.
1967            let avx = unsafe { a.then_avx8(&b) };
1968            assert_mat_approx(&scalar, &avx, 1e-3);
1969            // SAFETY: same runtime check as above.
1970            let unit = unsafe { ComputedTransform3D::IDENTITY.then_avx8(&b) };
1971            assert_mat_approx(&unit, &b, 1e-4);
1972        }
1973    }
1974
1975    #[cfg(all(target_arch = "x86_64", not(miri)))]
1976    #[test]
1977    fn simd_paths_do_not_panic_on_extreme_matrices() {
1978        // NaN/inf must flow through the intrinsics exactly like the scalar path:
1979        // no trap, no panic. (Values are not compared against the scalar path
1980        // because `then` fuses with mul_add while the SIMD kernels do not, and
1981        // fusion legitimately changes which NaN/inf a saturating term produces.)
1982        let extremes = [filled(f32::NAN), filled(f32::INFINITY), filled(1e30), filled(f32::MIN)];
1983        for a in &extremes {
1984            for b in &extremes {
1985                if std::is_x86_feature_detected!("sse") {
1986                    // SAFETY: SSE availability was just checked at runtime.
1987                    let r = unsafe { a.then_sse(b) };
1988                    core::hint::black_box(r);
1989                }
1990                if std::is_x86_feature_detected!("avx") {
1991                    // SAFETY: AVX availability was just checked at runtime.
1992                    let r = unsafe { a.then_avx8(b) };
1993                    core::hint::black_box(r);
1994                }
1995            }
1996        }
1997    }
1998
1999    #[cfg(all(target_arch = "x86_64", not(miri)))]
2000    #[test]
2001    fn linear_combine_avx8_computes_two_rows_at_once() {
2002        use core::arch::x86_64::{_mm256_loadu_ps, _mm256_storeu_ps};
2003
2004        if !std::is_x86_feature_detected!("avx") {
2005            return;
2006        }
2007        let b = ComputedTransform3D::new(
2008            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
2009        );
2010        let row0 = [1.0f32, 0.0, -2.0, 3.0];
2011        let row1 = [0.5f32, 4.0, 0.0, -1.0];
2012        let packed: [f32; 8] = [
2013            row0[0], row0[1], row0[2], row0[3], row1[0], row1[1], row1[2], row1[3],
2014        ];
2015        let mut out = [0.0f32; 8];
2016
2017        // SAFETY: AVX availability was just checked at runtime; `packed`/`out` are
2018        // live 8-f32 locals and both intrinsics used here are *unaligned* accesses.
2019        unsafe {
2020            let a01 = _mm256_loadu_ps(packed.as_ptr());
2021            let res = ComputedTransform3D::linear_combine_avx8(a01, &b);
2022            _mm256_storeu_ps(out.as_mut_ptr(), res);
2023        }
2024
2025        let want0 = naive_row_combine(row0, &b);
2026        let want1 = naive_row_combine(row1, &b);
2027        for c in 0..4 {
2028            assert!((out[c] - want0[c]).abs() < 1e-3, "low lane {c}: {} vs {}", out[c], want0[c]);
2029            assert!(
2030                (out[4 + c] - want1[c]).abs() < 1e-3,
2031                "high lane {c}: {} vs {}",
2032                out[4 + c],
2033                want1[c]
2034            );
2035        }
2036    }
2037
2038    // ------------------------------------------------ from_style_transform_vec
2039
2040    #[test]
2041    fn from_style_transform_vec_empty_is_identity() {
2042        let t = ComputedTransform3D::from_style_transform_vec(
2043            &[],
2044            &StyleTransformOrigin::default(),
2045            100.0,
2046            100.0,
2047            RotationMode::ForWebRender,
2048        );
2049        assert_eq!(t, ComputedTransform3D::IDENTITY);
2050    }
2051
2052    #[test]
2053    fn from_style_transform_vec_accumulates_a_thousand_translations_exactly() {
2054        // 1000 x translateX(1px): integer translations compose exactly on every
2055        // code path (scalar, SSE, AVX), so this must land on exactly 1000.0.
2056        let list = vec![StyleTransform::TranslateX(PixelValue::const_px(1)); 1000];
2057        let t = ComputedTransform3D::from_style_transform_vec(
2058            &list,
2059            &StyleTransformOrigin::default(),
2060            0.0,
2061            0.0,
2062            RotationMode::ForHitTesting,
2063        );
2064        assert_eq!(t.m[3][0], 1000.0);
2065        assert_eq!(t.m[3][1], 0.0);
2066        assert_eq!(t.m[0][0], 1.0);
2067    }
2068
2069    #[test]
2070    fn from_style_transform_vec_resolves_percentages_against_each_axis() {
2071        let list = vec![
2072            StyleTransform::TranslateX(PixelValue::const_percent(50)),
2073            StyleTransform::TranslateY(PixelValue::const_percent(50)),
2074        ];
2075        let t = ComputedTransform3D::from_style_transform_vec(
2076            &list,
2077            &StyleTransformOrigin::default(),
2078            200.0,
2079            80.0,
2080            RotationMode::ForHitTesting,
2081        );
2082        assert_eq!(t.m[3][0], 100.0); // 50% of the X basis
2083        assert_eq!(t.m[3][1], 40.0); // 50% of the Y basis
2084    }
2085
2086    #[test]
2087    fn from_style_transform_vec_with_extreme_percent_basis_does_not_panic() {
2088        let list = vec![StyleTransform::TranslateX(PixelValue::const_percent(50))];
2089        let run = |basis: f32| {
2090            ComputedTransform3D::from_style_transform_vec(
2091                &list,
2092                &StyleTransformOrigin::default(),
2093                basis,
2094                basis,
2095                RotationMode::ForWebRender,
2096            )
2097        };
2098
2099        // Any *finite* basis, however absurd, leaves the linear part an identity.
2100        for basis in [f32::MAX, f32::MIN, 0.0, -0.0, -1e30] {
2101            let t = run(basis);
2102            assert_eq!(t.m[0][0], 1.0, "linear part corrupted for basis {basis}");
2103            assert!(!t.m[3][0].is_nan(), "finite basis {basis} produced a NaN offset");
2104        }
2105
2106        // A NaN basis yields a NaN offset — defined, and no panic.
2107        assert!(run(f32::NAN).m[3][0].is_nan());
2108
2109        // HAZARD, asserted as-is: an *infinite* basis does not just give an
2110        // infinite offset — `then` multiplies the identity's zero w-column by it
2111        // (0.0 * inf == NaN), so the NaN spreads into the linear part as well.
2112        // Same on the scalar, SSE and AVX paths.
2113        let inf = run(f32::INFINITY);
2114        assert!(inf.m[0][0].is_nan(), "0.0 * inf should poison m11, got {}", inf.m[0][0]);
2115    }
2116
2117    #[test]
2118    fn from_style_transform_vec_long_mixed_list_does_not_panic() {
2119        let mut list = vec![];
2120        for i in 0..512 {
2121            list.push(match i % 4 {
2122                0 => StyleTransform::Rotate(AngleValue::const_deg(37)),
2123                1 => StyleTransform::Scale(StyleTransformScale2D {
2124                    x: FloatValue::const_new(1),
2125                    y: FloatValue::const_new(1),
2126                }),
2127                2 => StyleTransform::SkewX(AngleValue::const_deg(5)),
2128                _ => StyleTransform::TranslateY(PixelValue::const_px(1)),
2129            });
2130        }
2131        let t = ComputedTransform3D::from_style_transform_vec(
2132            &list,
2133            &StyleTransformOrigin::default(),
2134            300.0,
2135            150.0,
2136            RotationMode::ForWebRender,
2137        );
2138        // 512 chained f32 multiplies may drift, but must never produce NaN.
2139        assert!(!t.m[3][3].is_nan());
2140    }
2141
2142    // ---------------------------------------------------- from_style_transform
2143
2144    #[test]
2145    fn from_style_transform_matrix_2d_and_3d() {
2146        let m2d = StyleTransform::Matrix(StyleTransformMatrix2D {
2147            a: FloatValue::const_new(2),
2148            b: FloatValue::const_new(3),
2149            c: FloatValue::const_new(4),
2150            d: FloatValue::const_new(5),
2151            tx: FloatValue::const_new(6),
2152            ty: FloatValue::const_new(7),
2153        });
2154        let t = build(&m2d, 0.0, 0.0);
2155        assert_eq!(t.m[0], [2.0, 3.0, 0.0, 0.0]);
2156        assert_eq!(t.m[1], [4.0, 5.0, 0.0, 0.0]);
2157        assert_eq!(t.m[3], [6.0, 7.0, 0.0, 1.0]);
2158
2159        // The default matrix3d() is the identity.
2160        let m3d = StyleTransform::Matrix3D(StyleTransformMatrix3D::default());
2161        assert_eq!(build(&m3d, 0.0, 0.0), ComputedTransform3D::IDENTITY);
2162    }
2163
2164    #[test]
2165    fn from_style_transform_translate_units() {
2166        // px
2167        let t = build(&StyleTransform::TranslateX(PixelValue::const_px(25)), 0.0, 0.0);
2168        assert_eq!(t.m[3][0], 25.0);
2169        // em: resolved against DEFAULT_FONT_SIZE (16px)
2170        let t = build(&StyleTransform::TranslateY(PixelValue::const_em(2)), 0.0, 0.0);
2171        assert_eq!(t.m[3][1], 32.0);
2172        // 2D translate with mixed units
2173        let t = build(
2174            &StyleTransform::Translate(StyleTransformTranslate2D {
2175                x: PixelValue::const_percent(50),
2176                y: PixelValue::const_px(-10),
2177            }),
2178            400.0,
2179            0.0,
2180        );
2181        assert_eq!(t.m[3][0], 200.0);
2182        assert_eq!(t.m[3][1], -10.0);
2183    }
2184
2185    #[test]
2186    fn from_style_transform_translate_z_percent_falls_back_to_the_x_basis() {
2187        // Documented: "CSS has no containing block for Z-axis percentages; use X".
2188        // percent_resolve_y is deliberately different so a Y-basis regression fails.
2189        let t = build(&StyleTransform::TranslateZ(PixelValue::const_percent(50)), 200.0, 999.0);
2190        assert_eq!(t.m[3][2], 100.0, "translateZ(%) must resolve against the X basis");
2191
2192        let t3d = build(
2193            &StyleTransform::Translate3D(StyleTransformTranslate3D {
2194                x: PixelValue::const_px(0),
2195                y: PixelValue::const_px(0),
2196                z: PixelValue::const_percent(50),
2197            }),
2198            200.0,
2199            999.0,
2200        );
2201        assert_eq!(t3d.m[3][2], 100.0);
2202    }
2203
2204    #[test]
2205    fn from_style_transform_viewport_units_resolve_to_zero() {
2206        // to_pixels_internal() has no viewport context and documents a 0.0 result
2207        // for vw/vh/vmin/vmax — assert that (a silently-dropped translation), so
2208        // a future viewport-aware fix has to update this test on purpose.
2209        let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
2210        let t = build(&StyleTransform::TranslateX(vw), 1000.0, 1000.0);
2211        assert_eq!(t.m[3][0], 0.0);
2212    }
2213
2214    #[test]
2215    fn from_style_transform_saturating_pixel_values_stay_finite() {
2216        // PixelValue stores an isize (value * 1000), so an infinite CSS length
2217        // saturates at isize::MAX/1000 instead of becoming inf, and NaN becomes 0.
2218        let inf = build(&StyleTransform::TranslateX(PixelValue::px(f32::INFINITY)), 0.0, 0.0);
2219        assert!(
2220            inf.m[3][0].is_finite() && inf.m[3][0] > 1e6,
2221            "an infinite px length must saturate, got {}",
2222            inf.m[3][0]
2223        );
2224
2225        let nan = build(&StyleTransform::TranslateX(PixelValue::px(f32::NAN)), 0.0, 0.0);
2226        assert_eq!(nan.m[3][0], 0.0, "NaN px must saturate to 0, not propagate");
2227    }
2228
2229    #[test]
2230    fn from_style_transform_scale_variants() {
2231        let s2d = build(
2232            &StyleTransform::Scale(StyleTransformScale2D {
2233                x: FloatValue::const_new(2),
2234                y: FloatValue::const_new(3),
2235            }),
2236            0.0,
2237            0.0,
2238        );
2239        assert_eq!((s2d.m[0][0], s2d.m[1][1], s2d.m[2][2]), (2.0, 3.0, 1.0));
2240
2241        let s3d = build(
2242            &StyleTransform::Scale3D(StyleTransformScale3D {
2243                x: FloatValue::const_new(2),
2244                y: FloatValue::const_new(3),
2245                z: FloatValue::const_new(4),
2246            }),
2247            0.0,
2248            0.0,
2249        );
2250        assert_eq!((s3d.m[0][0], s3d.m[1][1], s3d.m[2][2]), (2.0, 3.0, 4.0));
2251
2252        // scaleX/Y/Z take a PercentageValue: 150% => 1.5, and only one axis moves.
2253        let sx = build(&StyleTransform::ScaleX(PercentageValue::const_new(150)), 0.0, 0.0);
2254        assert_eq!((sx.m[0][0], sx.m[1][1], sx.m[2][2]), (1.5, 1.0, 1.0));
2255        let sy = build(&StyleTransform::ScaleY(PercentageValue::const_new(150)), 0.0, 0.0);
2256        assert_eq!((sy.m[0][0], sy.m[1][1], sy.m[2][2]), (1.0, 1.5, 1.0));
2257        let sz = build(&StyleTransform::ScaleZ(PercentageValue::const_new(150)), 0.0, 0.0);
2258        assert_eq!((sz.m[0][0], sz.m[1][1], sz.m[2][2]), (1.0, 1.0, 1.5));
2259    }
2260
2261    #[test]
2262    fn from_style_transform_scale_zero_is_singular() {
2263        let s = build(
2264            &StyleTransform::Scale3D(StyleTransformScale3D {
2265                x: FloatValue::const_new(0),
2266                y: FloatValue::const_new(0),
2267                z: FloatValue::const_new(0),
2268            }),
2269            0.0,
2270            0.0,
2271        );
2272        assert_eq!(s.determinant(), 0.0);
2273        assert_eq!(s.inverse(), ComputedTransform3D::IDENTITY);
2274        // A collapsed element still maps points (to the origin), it does not panic.
2275        let p = s.transform_point2d(LogicalPosition::new(5.0, 9.0)).unwrap();
2276        assert_eq!((p.x, p.y), (0.0, 0.0));
2277    }
2278
2279    #[test]
2280    fn from_style_transform_skew_variants() {
2281        let sx = build(&StyleTransform::SkewX(AngleValue::const_deg(45)), 0.0, 0.0);
2282        assert!((sx.m[1][0] - 1.0).abs() < 1e-5, "skewX => tan(a) at m21");
2283        assert_eq!(sx.m[0][1], 0.0);
2284
2285        let sy = build(&StyleTransform::SkewY(AngleValue::const_deg(45)), 0.0, 0.0);
2286        assert!((sy.m[0][1] - 1.0).abs() < 1e-5, "skewY => tan(b) at m12");
2287        assert_eq!(sy.m[1][0], 0.0);
2288
2289        let sk = build(
2290            &StyleTransform::Skew(StyleTransformSkew2D {
2291                x: AngleValue::const_deg(30),
2292                y: AngleValue::const_deg(60),
2293            }),
2294            0.0,
2295            0.0,
2296        );
2297        assert!((sk.m[1][0] - 0.577_350_3).abs() < 1e-3); // tan(30deg)
2298        assert!((sk.m[0][1] - 1.732_050_8).abs() < 1e-3); // tan(60deg)
2299    }
2300
2301    #[test]
2302    fn from_style_transform_skew_90_degrees_stays_finite() {
2303        let sk = build(&StyleTransform::SkewX(AngleValue::const_deg(90)), 0.0, 0.0);
2304        assert!(all_finite(&sk), "skewX(90deg) must not produce inf/NaN: {sk:?}");
2305    }
2306
2307    #[test]
2308    fn from_style_transform_perspective_zero_is_infinite() {
2309        let p = build(&StyleTransform::Perspective(PixelValue::const_px(0)), 0.0, 0.0);
2310        // perspective(0) => -1/0 => -inf. No panic, but a poisoned matrix.
2311        assert!(p.m[2][3].is_infinite() && p.m[2][3].is_sign_negative());
2312
2313        let ok = build(&StyleTransform::Perspective(PixelValue::const_px(100)), 0.0, 0.0);
2314        assert!((ok.m[2][3] - (-0.01)).abs() < 1e-6);
2315    }
2316
2317    #[test]
2318    fn from_style_transform_rotate_degenerate_axis_is_identity() {
2319        // rotate3d(0, 0, 0, 45deg) has no axis to rotate about; it must degenerate
2320        // to the identity (modulo the origin round-trip), not to NaN.
2321        let r = ComputedTransform3D::from_style_transform(
2322            &StyleTransform::Rotate3D(StyleTransformRotate3D {
2323                x: FloatValue::const_new(0),
2324                y: FloatValue::const_new(0),
2325                z: FloatValue::const_new(0),
2326                angle: AngleValue::const_deg(45),
2327            }),
2328            &StyleTransformOrigin::default(), // 50% / 50% => (50, 50)
2329            100.0,
2330            100.0,
2331            RotationMode::ForHitTesting,
2332        );
2333        assert_mat_approx(&r, &ComputedTransform3D::IDENTITY, 1e-4);
2334    }
2335
2336    #[test]
2337    fn from_style_transform_rotate_angle_metrics_agree() {
2338        // 90deg == 0.25turn == 100grad. (Radians are NOT compared exactly here:
2339        // FloatValue quantizes to 3 decimals, so PI/2 stores as 1.570 rad
2340        // = 89.95deg — hence the looser tolerance on the rad case below.)
2341        let deg = build(&StyleTransform::Rotate(AngleValue::const_deg(90)), 0.0, 0.0);
2342        let turn = build(&StyleTransform::RotateZ(AngleValue::turn(0.25)), 0.0, 0.0);
2343        let grad = build(&StyleTransform::Rotate(AngleValue::const_grad(100)), 0.0, 0.0);
2344        assert_mat_approx(&deg, &turn, 1e-5);
2345        assert_mat_approx(&deg, &grad, 1e-5);
2346
2347        let rad = build(
2348            &StyleTransform::Rotate(AngleValue::rad(core::f32::consts::FRAC_PI_2)),
2349            0.0,
2350            0.0,
2351        );
2352        assert_mat_approx(&deg, &rad, 1e-2);
2353    }
2354
2355    #[test]
2356    fn from_style_transform_full_turn_normalizes_to_no_rotation() {
2357        // AngleValue::to_degrees() wraps into [0, 360), so 720deg == 0deg.
2358        let full = build(&StyleTransform::Rotate(AngleValue::const_deg(720)), 0.0, 0.0);
2359        assert_mat_approx(&full, &ComputedTransform3D::IDENTITY, 1e-3);
2360
2361        // ... and a negative angle wraps to its positive equivalent (-90 => 270).
2362        let neg = build(&StyleTransform::Rotate(AngleValue::const_deg(-90)), 0.0, 0.0);
2363        let pos = build(&StyleTransform::Rotate(AngleValue::const_deg(270)), 0.0, 0.0);
2364        assert_mat_approx(&neg, &pos, 1e-5);
2365    }
2366
2367    #[test]
2368    fn from_style_transform_rotate_x_y_z_pick_distinct_axes() {
2369        let rx = build(&StyleTransform::RotateX(AngleValue::const_deg(90)), 0.0, 0.0);
2370        let ry = build(&StyleTransform::RotateY(AngleValue::const_deg(90)), 0.0, 0.0);
2371        let rz = build(&StyleTransform::RotateZ(AngleValue::const_deg(90)), 0.0, 0.0);
2372        // Each keeps its own axis fixed: rotateX leaves m11, rotateY leaves m22,
2373        // rotateZ leaves m33 equal to 1.
2374        assert!((rx.m[0][0] - 1.0).abs() < 1e-5);
2375        assert!((ry.m[1][1] - 1.0).abs() < 1e-5);
2376        assert!((rz.m[2][2] - 1.0).abs() < 1e-5);
2377        // ... and they are genuinely different matrices.
2378        assert!(rx != ry && ry != rz && rx != rz);
2379        // Every rotation preserves volume.
2380        for r in [rx, ry, rz] {
2381            assert!((r.determinant() - 1.0).abs() < 1e-3, "det = {}", r.determinant());
2382        }
2383    }
2384
2385    #[test]
2386    fn from_style_transform_huge_angle_stays_finite() {
2387        // AngleValue saturates through FloatValue's isize backing, and to_degrees()
2388        // wraps modulo 360 — so even a "f32::MAX degrees" rotation is well-defined.
2389        let huge = build(&StyleTransform::Rotate(AngleValue::deg(f32::MAX)), 0.0, 0.0);
2390        assert!(all_finite(&huge), "huge angle produced inf/NaN: {huge:?}");
2391    }
2392
2393    // ----------------------------------------------------------- make_rotation
2394
2395    #[test]
2396    fn make_rotation_zero_degrees_is_identity_about_any_origin() {
2397        for mode in [RotationMode::ForWebRender, RotationMode::ForHitTesting] {
2398            let r = ComputedTransform3D::make_rotation((10.0, 20.0), 0.0, 0.0, 0.0, 1.0, mode);
2399            assert_mat_approx(&r, &ComputedTransform3D::IDENTITY, 1e-3);
2400        }
2401    }
2402
2403    #[test]
2404    fn make_rotation_modes_are_mutual_inverses() {
2405        // ForWebRender negates the angle, ForHitTesting does not — so composing
2406        // the two about the same origin must cancel out to the identity.
2407        let origin = (100.0, 50.0);
2408        let wr =
2409            ComputedTransform3D::make_rotation(origin, 45.0, 0.0, 0.0, 1.0, RotationMode::ForWebRender);
2410        let ht = ComputedTransform3D::make_rotation(
2411            origin,
2412            45.0,
2413            0.0,
2414            0.0,
2415            1.0,
2416            RotationMode::ForHitTesting,
2417        );
2418        assert!(wr != ht, "the two rotation modes must not produce the same matrix");
2419        assert_mat_approx(&wr.then(&ht), &ComputedTransform3D::IDENTITY, 1e-3);
2420    }
2421
2422    #[test]
2423    fn make_rotation_keeps_its_origin_fixed() {
2424        // The defining property of a rotation about a point: that point does not move.
2425        let r = ComputedTransform3D::make_rotation(
2426            (30.0, 40.0),
2427            90.0,
2428            0.0,
2429            0.0,
2430            1.0,
2431            RotationMode::ForHitTesting,
2432        );
2433        let p = r.transform_point2d(LogicalPosition::new(30.0, 40.0)).unwrap();
2434        assert!((p.x - 30.0).abs() < 1e-2, "origin moved in x: {}", p.x);
2435        assert!((p.y - 40.0).abs() < 1e-2, "origin moved in y: {}", p.y);
2436    }
2437
2438    #[test]
2439    fn make_rotation_preserves_volume() {
2440        let r = ComputedTransform3D::make_rotation(
2441            (7.0, -3.0),
2442            123.456,
2443            0.0,
2444            0.0,
2445            1.0,
2446            RotationMode::ForWebRender,
2447        );
2448        assert!((r.determinant() - 1.0).abs() < 1e-3, "det = {}", r.determinant());
2449    }
2450
2451    #[test]
2452    fn make_rotation_nan_degrees_does_not_panic() {
2453        let r = ComputedTransform3D::make_rotation(
2454            (1.0, 2.0),
2455            f32::NAN,
2456            0.0,
2457            0.0,
2458            1.0,
2459            RotationMode::ForWebRender,
2460        );
2461        assert!(r.m[0][0].is_nan(), "NaN degrees must propagate, not panic");
2462    }
2463
2464    #[test]
2465    fn make_rotation_infinite_degrees_does_not_panic() {
2466        for deg in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
2467            let r = ComputedTransform3D::make_rotation(
2468                (0.0, 0.0),
2469                deg,
2470                0.0,
2471                0.0,
2472                1.0,
2473                RotationMode::ForHitTesting,
2474            );
2475            core::hint::black_box(r);
2476        }
2477    }
2478
2479    #[test]
2480    fn make_rotation_extreme_origin_does_not_panic() {
2481        for origin in [
2482            (f32::MAX, f32::MAX),
2483            (f32::INFINITY, f32::NEG_INFINITY),
2484            (f32::NAN, 0.0),
2485        ] {
2486            let r = ComputedTransform3D::make_rotation(
2487                origin,
2488                45.0,
2489                0.0,
2490                0.0,
2491                1.0,
2492                RotationMode::ForWebRender,
2493            );
2494            core::hint::black_box(r);
2495        }
2496    }
2497
2498    #[test]
2499    fn make_rotation_degenerate_axis_is_a_pure_origin_round_trip() {
2500        // A zero axis cancels every rotation term, leaving T(-o) * I * T(o) = I.
2501        let r = ComputedTransform3D::make_rotation(
2502            (12.0, 34.0),
2503            90.0,
2504            0.0,
2505            0.0,
2506            0.0,
2507            RotationMode::ForHitTesting,
2508        );
2509        assert_mat_approx(&r, &ComputedTransform3D::IDENTITY, 1e-4);
2510    }
2511}