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