1use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
15
16use azul_css::props::style::{StyleTransform, StyleTransformOrigin};
17
18use crate::geom::LogicalPosition;
19
20pub static INITIALIZED: AtomicBool = AtomicBool::new(false);
22pub static USE_AVX: AtomicBool = AtomicBool::new(false);
24pub static USE_SSE: AtomicBool = AtomicBool::new(false);
26
27#[derive(Debug, Copy, Clone)]
33pub enum RotationMode {
34 ForWebRender,
36 ForHitTesting,
38}
39
40#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
56#[repr(C)]
57pub struct ComputedTransform3D {
58 pub m: [[f32; 4]; 4],
60}
61
62impl ComputedTransform3D {
63 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 #[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 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 #[must_use]
123 #[allow(clippy::suboptimal_flops)] 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)] fn determinant(&self) -> f32 {
227 let m = |i: usize, j: usize| f64::from(self.m[i][j]);
233 let det = m(0, 3) * m(1, 2) * m(2, 1) * m(3, 0)
234 - m(0, 2) * m(1, 3) * m(2, 1) * m(3, 0)
235 - m(0, 3) * m(1, 1) * m(2, 2) * m(3, 0)
236 + m(0, 1) * m(1, 3) * m(2, 2) * m(3, 0)
237 + m(0, 2) * m(1, 1) * m(2, 3) * m(3, 0)
238 - m(0, 1) * m(1, 2) * m(2, 3) * m(3, 0)
239 - m(0, 3) * m(1, 2) * m(2, 0) * m(3, 1)
240 + m(0, 2) * m(1, 3) * m(2, 0) * m(3, 1)
241 + m(0, 3) * m(1, 0) * m(2, 2) * m(3, 1)
242 - m(0, 0) * m(1, 3) * m(2, 2) * m(3, 1)
243 - m(0, 2) * m(1, 0) * m(2, 3) * m(3, 1)
244 + m(0, 0) * m(1, 2) * m(2, 3) * m(3, 1)
245 + m(0, 3) * m(1, 1) * m(2, 0) * m(3, 2)
246 - m(0, 1) * m(1, 3) * m(2, 0) * m(3, 2)
247 - m(0, 3) * m(1, 0) * m(2, 1) * m(3, 2)
248 + m(0, 0) * m(1, 3) * m(2, 1) * m(3, 2)
249 + m(0, 1) * m(1, 0) * m(2, 3) * m(3, 2)
250 - m(0, 0) * m(1, 1) * m(2, 3) * m(3, 2)
251 - m(0, 2) * m(1, 1) * m(2, 0) * m(3, 3)
252 + m(0, 1) * m(1, 2) * m(2, 0) * m(3, 3)
253 + m(0, 2) * m(1, 0) * m(2, 1) * m(3, 3)
254 - m(0, 0) * m(1, 2) * m(2, 1) * m(3, 3)
255 - m(0, 1) * m(1, 0) * m(2, 2) * m(3, 3)
256 + m(0, 0) * m(1, 1) * m(2, 2) * m(3, 3);
257 #[allow(clippy::cast_possible_truncation)] let det = det as f32;
259 det
260 }
261
262 fn multiply_scalar(&self, x: f32) -> Self {
263 Self::new(
264 self.m[0][0] * x,
265 self.m[0][1] * x,
266 self.m[0][2] * x,
267 self.m[0][3] * x,
268 self.m[1][0] * x,
269 self.m[1][1] * x,
270 self.m[1][2] * x,
271 self.m[1][3] * x,
272 self.m[2][0] * x,
273 self.m[2][1] * x,
274 self.m[2][2] * x,
275 self.m[2][3] * x,
276 self.m[3][0] * x,
277 self.m[3][1] * x,
278 self.m[3][2] * x,
279 self.m[3][3] * x,
280 )
281 }
282
283 pub fn from_style_transform_vec(
285 t_vec: &[StyleTransform],
286 transform_origin: &StyleTransformOrigin,
287 percent_resolve_x: f32,
288 percent_resolve_y: f32,
289 rotation_mode: RotationMode,
290 ) -> Self {
291 let mut matrix = Self::IDENTITY;
303 let use_avx =
304 INITIALIZED.load(AtomicOrdering::Relaxed) && USE_AVX.load(AtomicOrdering::Relaxed);
305 let use_sse = !use_avx
306 && INITIALIZED.load(AtomicOrdering::Relaxed)
307 && USE_SSE.load(AtomicOrdering::Relaxed);
308
309 if use_avx {
310 for t in t_vec {
311 #[cfg(target_arch = "x86_64")]
315 unsafe {
316 matrix = matrix.then_avx8(&Self::from_style_transform(
317 t,
318 transform_origin,
319 percent_resolve_x,
320 percent_resolve_y,
321 rotation_mode,
322 ));
323 }
324 }
325 } else if use_sse {
326 for t in t_vec {
327 #[cfg(target_arch = "x86_64")]
331 unsafe {
332 matrix = matrix.then_sse(&Self::from_style_transform(
333 t,
334 transform_origin,
335 percent_resolve_x,
336 percent_resolve_y,
337 rotation_mode,
338 ));
339 }
340 }
341 } else {
342 for t in t_vec {
344 matrix = matrix.then(&Self::from_style_transform(
345 t,
346 transform_origin,
347 percent_resolve_x,
348 percent_resolve_y,
349 rotation_mode,
350 ));
351 }
352 }
353
354 matrix
355 }
356
357 #[allow(clippy::many_single_char_names)] #[allow(clippy::too_many_lines)] fn from_style_transform(
362 t: &StyleTransform,
363 transform_origin: &StyleTransformOrigin,
364 percent_resolve_x: f32,
365 percent_resolve_y: f32,
366 rotation_mode: RotationMode,
367 ) -> Self {
368 use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
369 use azul_css::props::style::StyleTransform::{Matrix, Matrix3D, Translate, Translate3D, TranslateX, TranslateY, TranslateZ, Rotate3D, RotateX, RotateY, Rotate, RotateZ, Scale, Scale3D, ScaleX, ScaleY, ScaleZ, Skew, SkewX, SkewY, Perspective};
370 match t {
371 Matrix(mat2d) => {
372 let a = mat2d.a.get();
373 let b = mat2d.b.get();
374 let c = mat2d.c.get();
375 let d = mat2d.d.get();
376 let tx = mat2d.tx.get();
377 let ty = mat2d.ty.get();
378
379 Self::new_2d(a, b, c, d, tx, ty)
380 }
381 Matrix3D(mat3d) => {
382 let m11 = mat3d.m11.get();
383 let m12 = mat3d.m12.get();
384 let m13 = mat3d.m13.get();
385 let m14 = mat3d.m14.get();
386 let m21 = mat3d.m21.get();
387 let m22 = mat3d.m22.get();
388 let m23 = mat3d.m23.get();
389 let m24 = mat3d.m24.get();
390 let m31 = mat3d.m31.get();
391 let m32 = mat3d.m32.get();
392 let m33 = mat3d.m33.get();
393 let m34 = mat3d.m34.get();
394 let m41 = mat3d.m41.get();
395 let m42 = mat3d.m42.get();
396 let m43 = mat3d.m43.get();
397 let m44 = mat3d.m44.get();
398
399 Self::new(
400 m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44,
401 )
402 }
403 Translate(trans2d) => {
404
405 Self::new_translation(
406 trans2d
407 .x
408 .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
409 trans2d
410 .y
411 .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
412 0.0,
413 )
414 }
415 Translate3D(trans3d) => {
416
417 Self::new_translation(
418 trans3d
419 .x
420 .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
421 trans3d
422 .y
423 .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
424 trans3d
425 .z
426 .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
428 )
429 }
430 TranslateX(trans_x) => {
431
432 Self::new_translation(
433 trans_x.to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
434 0.0,
435 0.0,
436 )
437 }
438 TranslateY(trans_y) => {
439
440 Self::new_translation(
441 0.0,
442 trans_y.to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
443 0.0,
444 )
445 }
446 TranslateZ(trans_z) => {
447
448 Self::new_translation(
449 0.0,
450 0.0,
451 trans_z.to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
452 )
453 } Rotate3D(rot3d) => {
455
456 let rotation_origin = (
457 transform_origin
458 .x
459 .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
460 transform_origin
461 .y
462 .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
463 );
464 Self::make_rotation(
465 rotation_origin,
466 rot3d.angle.to_degrees(),
467 rot3d.x.get(),
468 rot3d.y.get(),
469 rot3d.z.get(),
470 rotation_mode,
471 )
472 }
473 RotateX(angle_x) => {
474
475 let rotation_origin = (
476 transform_origin
477 .x
478 .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
479 transform_origin
480 .y
481 .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
482 );
483 Self::make_rotation(
484 rotation_origin,
485 angle_x.to_degrees(),
486 1.0,
487 0.0,
488 0.0,
489 rotation_mode,
490 )
491 }
492 RotateY(angle_y) => {
493
494 let rotation_origin = (
495 transform_origin
496 .x
497 .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
498 transform_origin
499 .y
500 .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
501 );
502 Self::make_rotation(
503 rotation_origin,
504 angle_y.to_degrees(),
505 0.0,
506 1.0,
507 0.0,
508 rotation_mode,
509 )
510 }
511 Rotate(angle_z) | RotateZ(angle_z) => {
512
513 let rotation_origin = (
514 transform_origin
515 .x
516 .to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
517 transform_origin
518 .y
519 .to_pixels_internal(percent_resolve_y, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
520 );
521 Self::make_rotation(
522 rotation_origin,
523 angle_z.to_degrees(),
524 0.0,
525 0.0,
526 1.0,
527 rotation_mode,
528 )
529 }
530 Scale(scale2d) => Self::new_scale(scale2d.x.get(), scale2d.y.get(), 1.0),
531 Scale3D(scale3d) => Self::new_scale(scale3d.x.get(), scale3d.y.get(), scale3d.z.get()),
532 ScaleX(scale_x) => Self::new_scale(scale_x.normalized(), 1.0, 1.0),
533 ScaleY(scale_y) => Self::new_scale(1.0, scale_y.normalized(), 1.0),
534 ScaleZ(scale_z) => Self::new_scale(1.0, 1.0, scale_z.normalized()),
535 Skew(skew2d) => Self::new_skew(skew2d.x.to_degrees(), skew2d.y.to_degrees()),
536 SkewX(skew_x) => Self::new_skew(skew_x.to_degrees(), 0.0),
537 SkewY(skew_y) => Self::new_skew(0.0, skew_y.to_degrees()),
538 Perspective(px) => {
539
540 Self::new_perspective(px.to_pixels_internal(percent_resolve_x, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE))
541 }
542 }
543 }
544
545 #[must_use]
547 #[inline]
548 pub const fn new_scale(x: f32, y: f32, z: f32) -> Self {
549 Self::new(
550 x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, 1.0,
551 )
552 }
553
554 #[must_use]
556 #[inline]
557 pub const fn new_translation(x: f32, y: f32, z: f32) -> Self {
558 Self::new(
559 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0,
560 )
561 }
562
563 #[must_use]
565 #[inline]
566 fn new_perspective(d: f32) -> Self {
567 Self::new(
568 1.0,
569 0.0,
570 0.0,
571 0.0,
572 0.0,
573 1.0,
574 0.0,
575 0.0,
576 0.0,
577 0.0,
578 1.0,
579 -1.0 / d,
580 0.0,
581 0.0,
582 0.0,
583 1.0,
584 )
585 }
586
587 #[must_use]
590 #[inline]
591 #[allow(clippy::suboptimal_flops)] fn new_rotation(x: f32, y: f32, z: f32, theta_radians: f32) -> Self {
593 let xx = x * x;
594 let yy = y * y;
595 let zz = z * z;
596
597 let half_theta = theta_radians / 2.0;
598 let sc = half_theta.sin() * half_theta.cos();
599 let sq = half_theta.sin() * half_theta.sin();
600
601 Self::new(
602 1.0 - 2.0 * (yy + zz) * sq,
603 2.0 * (x * y * sq + z * sc),
604 2.0 * (x * z * sq - y * sc),
605 0.0,
606 2.0 * (x * y * sq - z * sc),
607 1.0 - 2.0 * (xx + zz) * sq,
608 2.0 * (y * z * sq + x * sc),
609 0.0,
610 2.0 * (x * z * sq + y * sc),
611 2.0 * (y * z * sq - x * sc),
612 1.0 - 2.0 * (xx + yy) * sq,
613 0.0,
614 0.0,
615 0.0,
616 0.0,
617 1.0,
618 )
619 }
620
621 #[must_use]
623 #[inline]
624 fn new_skew(alpha: f32, beta: f32) -> Self {
625 let (sx, sy) = (beta.to_radians().tan(), alpha.to_radians().tan());
626 Self::new(
627 1.0, sx, 0.0, 0.0, sy, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
628 )
629 }
630
631 #[must_use]
633 pub(crate) const fn get_column_major(&self) -> Self {
634 Self::new(
635 self.m[0][0],
636 self.m[1][0],
637 self.m[2][0],
638 self.m[3][0],
639 self.m[0][1],
640 self.m[1][1],
641 self.m[2][1],
642 self.m[3][1],
643 self.m[0][2],
644 self.m[1][2],
645 self.m[2][2],
646 self.m[3][2],
647 self.m[0][3],
648 self.m[1][3],
649 self.m[2][3],
650 self.m[3][3],
651 )
652 }
653
654 #[must_use]
656 pub fn transform_point2d(&self, p: LogicalPosition) -> Option<LogicalPosition> {
657 let w =
658 p.x.mul_add(self.m[0][3], p.y.mul_add(self.m[1][3], self.m[3][3]));
659
660 if !w.is_sign_positive() {
661 return None;
662 }
663
664 let x =
665 p.x.mul_add(self.m[0][0], p.y.mul_add(self.m[1][0], self.m[3][0]));
666 let y =
667 p.x.mul_add(self.m[0][1], p.y.mul_add(self.m[1][1], self.m[3][1]));
668
669 Some(LogicalPosition { x: x / w, y: y / w })
670 }
671
672 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
674 self.m[3][0] *= scale_factor;
676 self.m[3][1] *= scale_factor;
677 self.m[3][2] *= scale_factor;
678 }
679
680 #[must_use]
682 #[inline]
683 #[allow(clippy::too_many_lines)] pub fn then(&self, other: &Self) -> Self {
685 Self::new(
686 self.m[0][0].mul_add(
687 other.m[0][0],
688 self.m[0][1].mul_add(
689 other.m[1][0],
690 self.m[0][2].mul_add(other.m[2][0], self.m[0][3] * other.m[3][0]),
691 ),
692 ),
693 self.m[0][0].mul_add(
694 other.m[0][1],
695 self.m[0][1].mul_add(
696 other.m[1][1],
697 self.m[0][2].mul_add(other.m[2][1], self.m[0][3] * other.m[3][1]),
698 ),
699 ),
700 self.m[0][0].mul_add(
701 other.m[0][2],
702 self.m[0][1].mul_add(
703 other.m[1][2],
704 self.m[0][2].mul_add(other.m[2][2], self.m[0][3] * other.m[3][2]),
705 ),
706 ),
707 self.m[0][0].mul_add(
708 other.m[0][3],
709 self.m[0][1].mul_add(
710 other.m[1][3],
711 self.m[0][2].mul_add(other.m[2][3], self.m[0][3] * other.m[3][3]),
712 ),
713 ),
714 self.m[1][0].mul_add(
715 other.m[0][0],
716 self.m[1][1].mul_add(
717 other.m[1][0],
718 self.m[1][2].mul_add(other.m[2][0], self.m[1][3] * other.m[3][0]),
719 ),
720 ),
721 self.m[1][0].mul_add(
722 other.m[0][1],
723 self.m[1][1].mul_add(
724 other.m[1][1],
725 self.m[1][2].mul_add(other.m[2][1], self.m[1][3] * other.m[3][1]),
726 ),
727 ),
728 self.m[1][0].mul_add(
729 other.m[0][2],
730 self.m[1][1].mul_add(
731 other.m[1][2],
732 self.m[1][2].mul_add(other.m[2][2], self.m[1][3] * other.m[3][2]),
733 ),
734 ),
735 self.m[1][0].mul_add(
736 other.m[0][3],
737 self.m[1][1].mul_add(
738 other.m[1][3],
739 self.m[1][2].mul_add(other.m[2][3], self.m[1][3] * other.m[3][3]),
740 ),
741 ),
742 self.m[2][0].mul_add(
743 other.m[0][0],
744 self.m[2][1].mul_add(
745 other.m[1][0],
746 self.m[2][2].mul_add(other.m[2][0], self.m[2][3] * other.m[3][0]),
747 ),
748 ),
749 self.m[2][0].mul_add(
750 other.m[0][1],
751 self.m[2][1].mul_add(
752 other.m[1][1],
753 self.m[2][2].mul_add(other.m[2][1], self.m[2][3] * other.m[3][1]),
754 ),
755 ),
756 self.m[2][0].mul_add(
757 other.m[0][2],
758 self.m[2][1].mul_add(
759 other.m[1][2],
760 self.m[2][2].mul_add(other.m[2][2], self.m[2][3] * other.m[3][2]),
761 ),
762 ),
763 self.m[2][0].mul_add(
764 other.m[0][3],
765 self.m[2][1].mul_add(
766 other.m[1][3],
767 self.m[2][2].mul_add(other.m[2][3], self.m[2][3] * other.m[3][3]),
768 ),
769 ),
770 self.m[3][0].mul_add(
771 other.m[0][0],
772 self.m[3][1].mul_add(
773 other.m[1][0],
774 self.m[3][2].mul_add(other.m[2][0], self.m[3][3] * other.m[3][0]),
775 ),
776 ),
777 self.m[3][0].mul_add(
778 other.m[0][1],
779 self.m[3][1].mul_add(
780 other.m[1][1],
781 self.m[3][2].mul_add(other.m[2][1], self.m[3][3] * other.m[3][1]),
782 ),
783 ),
784 self.m[3][0].mul_add(
785 other.m[0][2],
786 self.m[3][1].mul_add(
787 other.m[1][2],
788 self.m[3][2].mul_add(other.m[2][2], self.m[3][3] * other.m[3][2]),
789 ),
790 ),
791 self.m[3][0].mul_add(
792 other.m[0][3],
793 self.m[3][1].mul_add(
794 other.m[1][3],
795 self.m[3][2].mul_add(other.m[2][3], self.m[3][3] * other.m[3][3]),
796 ),
797 ),
798 )
799 }
800
801 #[cfg(target_arch = "x86_64")]
813 #[inline]
814 unsafe fn linear_combine_sse(a: [f32; 4], b: &Self) -> [f32; 4] { unsafe {
815 use core::{
816 arch::x86_64::{__m128, _mm_add_ps, _mm_mul_ps, _mm_shuffle_ps},
817 mem,
818 };
819
820 let a: __m128 = mem::transmute(a);
821 let mut result = _mm_mul_ps(_mm_shuffle_ps(a, a, 0x00), mem::transmute::<[f32; 4], __m128>(b.m[0]));
822 result = _mm_add_ps(
823 result,
824 _mm_mul_ps(_mm_shuffle_ps(a, a, 0x55), mem::transmute::<[f32; 4], __m128>(b.m[1])),
825 );
826 result = _mm_add_ps(
827 result,
828 _mm_mul_ps(_mm_shuffle_ps(a, a, 0xaa), mem::transmute::<[f32; 4], __m128>(b.m[2])),
829 );
830 result = _mm_add_ps(
831 result,
832 _mm_mul_ps(_mm_shuffle_ps(a, a, 0xff), mem::transmute::<[f32; 4], __m128>(b.m[3])),
833 );
834
835 mem::transmute(result)
836 }}
837
838 #[cfg(target_arch = "x86_64")]
843 #[inline]
844 unsafe fn then_sse(&self, other: &Self) -> Self { unsafe {
845 Self {
846 m: [
847 Self::linear_combine_sse(self.m[0], other),
848 Self::linear_combine_sse(self.m[1], other),
849 Self::linear_combine_sse(self.m[2], other),
850 Self::linear_combine_sse(self.m[3], other),
851 ],
852 }
853 }}
854
855 #[cfg(target_arch = "x86_64")]
871 unsafe fn linear_combine_avx8(
872 a01: core::arch::x86_64::__m256,
873 b: &Self,
874 ) -> core::arch::x86_64::__m256 { unsafe {
875 use core::arch::x86_64::{
876 _mm256_add_ps, _mm256_loadu2_m128, _mm256_mul_ps, _mm256_shuffle_ps,
877 };
878
879 let broadcast_row = |row: &[f32; 4]| {
882 let p = row.as_ptr();
883 _mm256_loadu2_m128(p, p)
884 };
885
886 let mut result = _mm256_mul_ps(
887 _mm256_shuffle_ps(a01, a01, 0x00),
888 broadcast_row(&b.m[0]),
889 );
890 result = _mm256_add_ps(
891 result,
892 _mm256_mul_ps(_mm256_shuffle_ps(a01, a01, 0x55), broadcast_row(&b.m[1])),
893 );
894 result = _mm256_add_ps(
895 result,
896 _mm256_mul_ps(_mm256_shuffle_ps(a01, a01, 0xaa), broadcast_row(&b.m[2])),
897 );
898 result = _mm256_add_ps(
899 result,
900 _mm256_mul_ps(_mm256_shuffle_ps(a01, a01, 0xff), broadcast_row(&b.m[3])),
901 );
902 result
903 }}
904
905 #[cfg(target_arch = "x86_64")]
913 #[inline]
914 unsafe fn then_avx8(&self, other: &Self) -> Self { unsafe {
915 use core::{
916 arch::x86_64::{__m256, _mm256_loadu_ps, _mm256_storeu_ps, _mm256_zeroupper},
917 mem,
918 };
919
920 _mm256_zeroupper();
921
922 let a01: __m256 = _mm256_loadu_ps(&raw const self.m[0][0]);
923 let a23: __m256 = _mm256_loadu_ps(&raw const self.m[2][0]);
924
925 let out01x = Self::linear_combine_avx8(a01, other);
926 let out23x = Self::linear_combine_avx8(a23, other);
927
928 let mut out = Self {
929 m: [self.m[0], self.m[1], self.m[2], self.m[3]],
930 };
931
932 _mm256_storeu_ps(&raw mut out.m[0][0], out01x);
933 _mm256_storeu_ps(&raw mut out.m[2][0], out23x);
934
935 out
936 }}
937
938 #[must_use]
940 #[inline]
941 #[allow(clippy::suboptimal_flops)] fn make_rotation(
943 rotation_origin: (f32, f32),
944 mut degrees: f32,
945 axis_x: f32,
946 axis_y: f32,
947 axis_z: f32,
948 rotation_mode: RotationMode,
950 ) -> Self {
951 degrees = match rotation_mode {
952 RotationMode::ForWebRender => -degrees,
954 RotationMode::ForHitTesting => degrees,
956 };
957
958 let (origin_x, origin_y) = rotation_origin;
959 let pre_transform = Self::new_translation(-origin_x, -origin_y, 0.0);
960 let post_transform = Self::new_translation(origin_x, origin_y, 0.0);
961 let theta = 2.0_f32 * core::f32::consts::PI - degrees.to_radians();
962 let rotate_transform =
963 Self::new_rotation(axis_x, axis_y, axis_z, theta);
964
965 pre_transform.then(&rotate_transform).then(&post_transform)
966 }
967}
968
969#[cfg(test)]
970#[allow(clippy::items_after_statements, clippy::redundant_clone, clippy::cast_possible_truncation, clippy::cast_sign_loss, trivial_casts, clippy::borrow_as_ptr, clippy::cast_ptr_alignment, clippy::unused_self, unused_qualifications, unreachable_pub, private_interfaces)] mod audit_tests {
972 use super::*;
973
974 fn sample_a() -> ComputedTransform3D {
975 ComputedTransform3D::new(
976 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
977 )
978 }
979 fn sample_b() -> ComputedTransform3D {
980 ComputedTransform3D::new(
981 16.0, 15.0, 14.0, 13.0, 12.0, 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0,
982 )
983 }
984
985 fn approx_eq(a: &ComputedTransform3D, b: &ComputedTransform3D) {
986 for r in 0..4 {
987 for c in 0..4 {
988 assert!(
989 (a.m[r][c] - b.m[r][c]).abs() < 1e-3,
990 "mismatch at [{r}][{c}]: {} vs {}",
991 a.m[r][c],
992 b.m[r][c]
993 );
994 }
995 }
996 }
997
998 fn naive_then(a: &ComputedTransform3D, b: &ComputedTransform3D) -> ComputedTransform3D {
1002 let mut out = ComputedTransform3D::IDENTITY;
1003 for r in 0..4 {
1004 for c in 0..4 {
1005 let mut acc = 0.0f32;
1006 for k in 0..4 {
1007 acc += a.m[r][k] * b.m[k][c];
1008 }
1009 out.m[r][c] = acc;
1010 }
1011 }
1012 out
1013 }
1014
1015 #[test]
1019 fn scalar_matmul_matches_reference() {
1020 let a = sample_a();
1021 let b = sample_b();
1022 approx_eq(&a.then(&b), &naive_then(&a, &b));
1023 approx_eq(&ComputedTransform3D::IDENTITY.then(&b), &b);
1025 approx_eq(&a.then(&ComputedTransform3D::IDENTITY), &a);
1026 }
1027
1028 #[cfg(not(miri))]
1037 #[test]
1038 fn simd_matmul_matches_scalar() {
1039 let a = sample_a();
1040 let b = sample_b();
1041 let scalar = a.then(&b);
1042
1043 #[cfg(target_arch = "x86_64")]
1044 {
1045 if std::is_x86_feature_detected!("sse") {
1046 let sse = unsafe { a.then_sse(&b) };
1047 approx_eq(&scalar, &sse);
1048 }
1049 if std::is_x86_feature_detected!("avx") {
1050 let avx = unsafe { a.then_avx8(&b) };
1051 approx_eq(&scalar, &avx);
1052 }
1053 }
1054
1055 approx_eq(&ComputedTransform3D::IDENTITY.then(&b), &b);
1057 }
1058
1059 #[cfg(all(target_arch = "x86_64", not(miri)))]
1070 #[test]
1071 fn avx_result_independent_of_row_alignment() {
1072 if !std::is_x86_feature_detected!("avx") {
1073 return;
1074 }
1075
1076 let a = sample_a();
1077 let b = sample_b();
1078 let expected = unsafe { a.then_avx8(&b) };
1079
1080 const N: usize = core::mem::size_of::<ComputedTransform3D>(); let mut buf = vec![0u8; N * 2 + 16];
1082 let base = buf.as_mut_ptr();
1083
1084 unsafe {
1089 let aligned = base.add(base.align_offset(16));
1090 let misaligned = aligned.add(4); for off_ptr in [aligned, misaligned] {
1092 core::ptr::copy_nonoverlapping(
1093 (&raw const a).cast::<u8>(),
1094 off_ptr,
1095 N,
1096 );
1097 let a_ref = &*off_ptr.cast::<ComputedTransform3D>();
1098 let got = a_ref.then_avx8(&b);
1099 approx_eq(&expected, &got);
1100 }
1101 }
1102 }
1103}
1104
1105#[cfg(test)]
1106#[allow(
1107 clippy::float_cmp,
1108 clippy::unreadable_literal,
1109 clippy::excessive_precision,
1110 clippy::cast_possible_truncation,
1111 clippy::cast_precision_loss,
1112 clippy::too_many_lines,
1113 clippy::needless_range_loop,
1114 clippy::suboptimal_flops,
1115 unused_qualifications
1116)] mod autotest_generated {
1118 use azul_css::props::basic::{
1119 AngleValue, FloatValue, PercentageValue, PixelValue, SizeMetric,
1120 };
1121 use azul_css::props::style::{
1122 StyleTransformMatrix2D, StyleTransformMatrix3D, StyleTransformRotate3D,
1123 StyleTransformScale2D, StyleTransformScale3D, StyleTransformSkew2D,
1124 StyleTransformTranslate2D, StyleTransformTranslate3D,
1125 };
1126
1127 use super::*;
1128
1129 fn filled(v: f32) -> ComputedTransform3D {
1134 ComputedTransform3D { m: [[v; 4]; 4] }
1135 }
1136
1137 fn assert_mat_approx(a: &ComputedTransform3D, b: &ComputedTransform3D, tol: f32) {
1138 for r in 0..4 {
1139 for c in 0..4 {
1140 assert!(
1141 (a.m[r][c] - b.m[r][c]).abs() <= tol,
1142 "mismatch at [{r}][{c}]: {} vs {} (tol {tol})",
1143 a.m[r][c],
1144 b.m[r][c]
1145 );
1146 }
1147 }
1148 }
1149
1150 fn all_finite(t: &ComputedTransform3D) -> bool {
1151 t.m.iter().flatten().all(|v| v.is_finite())
1152 }
1153
1154 fn det3(m: [[f32; 3]; 3]) -> f32 {
1156 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
1157 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
1158 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
1159 }
1160
1161 fn det4_naive(t: &ComputedTransform3D) -> f32 {
1165 let mut sum = 0.0f32;
1166 for col in 0..4 {
1167 let mut minor = [[0.0f32; 3]; 3];
1168 for r in 1..4 {
1169 let mut cc = 0;
1170 for c in 0..4 {
1171 if c == col {
1172 continue;
1173 }
1174 minor[r - 1][cc] = t.m[r][c];
1175 cc += 1;
1176 }
1177 }
1178 let sign = if col % 2 == 0 { 1.0 } else { -1.0 };
1179 sum += sign * t.m[0][col] * det3(minor);
1180 }
1181 sum
1182 }
1183
1184 #[allow(dead_code)]
1187 fn naive_row_combine(a: [f32; 4], b: &ComputedTransform3D) -> [f32; 4] {
1188 let mut out = [0.0f32; 4];
1189 for c in 0..4 {
1190 for k in 0..4 {
1191 out[c] += a[k] * b.m[k][c];
1192 }
1193 }
1194 out
1195 }
1196
1197 fn origin_px(x: isize, y: isize) -> StyleTransformOrigin {
1198 StyleTransformOrigin {
1199 x: PixelValue::const_px(x),
1200 y: PixelValue::const_px(y),
1201 }
1202 }
1203
1204 fn build(t: &StyleTransform, px: f32, py: f32) -> ComputedTransform3D {
1207 ComputedTransform3D::from_style_transform(t, &origin_px(0, 0), px, py, RotationMode::ForHitTesting)
1208 }
1209
1210 #[test]
1213 fn new_stores_all_16_elements_row_major() {
1214 let t = ComputedTransform3D::new(
1215 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1216 );
1217 for r in 0..4 {
1218 for c in 0..4 {
1219 let expected = (r * 4 + c + 1) as f32;
1220 assert_eq!(t.m[r][c], expected, "row-major slot [{r}][{c}]");
1221 }
1222 }
1223 }
1224
1225 #[test]
1226 fn new_preserves_extreme_values_verbatim() {
1227 let t = ComputedTransform3D::new(
1228 f32::NAN,
1229 f32::INFINITY,
1230 f32::NEG_INFINITY,
1231 f32::MAX,
1232 f32::MIN,
1233 f32::MIN_POSITIVE,
1234 -0.0,
1235 0.0,
1236 f32::EPSILON,
1237 -f32::EPSILON,
1238 1e-45, -1e-45,
1240 f32::MAX,
1241 f32::MIN,
1242 f32::INFINITY,
1243 f32::NEG_INFINITY,
1244 );
1245 assert!(t.m[0][0].is_nan());
1247 assert!(t.m[0][1].is_infinite() && t.m[0][1].is_sign_positive());
1248 assert!(t.m[0][2].is_infinite() && t.m[0][2].is_sign_negative());
1249 assert_eq!(t.m[0][3], f32::MAX);
1250 assert_eq!(t.m[1][0], f32::MIN);
1251 assert_eq!(t.m[1][1], f32::MIN_POSITIVE);
1252 assert!(t.m[1][2].is_sign_negative());
1254 assert!(t.m[1][3].is_sign_positive());
1255 assert_eq!(t.m[2][0], f32::EPSILON);
1256 assert!(t.m[3][2].is_infinite());
1257 }
1258
1259 #[test]
1260 fn new_2d_matches_css_matrix_layout() {
1261 let t = ComputedTransform3D::new_2d(2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
1263 assert_eq!(t.m[0], [2.0, 3.0, 0.0, 0.0]);
1264 assert_eq!(t.m[1], [4.0, 5.0, 0.0, 0.0]);
1265 assert_eq!(t.m[2], [0.0, 0.0, 1.0, 0.0]); assert_eq!(t.m[3], [6.0, 7.0, 0.0, 1.0]);
1267 }
1268
1269 #[test]
1270 fn new_2d_with_extremes_keeps_z_row_intact() {
1271 let t = ComputedTransform3D::new_2d(
1272 f32::NAN,
1273 f32::INFINITY,
1274 f32::MAX,
1275 f32::MIN,
1276 f32::NEG_INFINITY,
1277 -0.0,
1278 );
1279 assert!(t.m[0][0].is_nan());
1280 assert_eq!(t.m[2], [0.0, 0.0, 1.0, 0.0]);
1282 assert_eq!(t.m[3][3], 1.0);
1283 }
1284
1285 #[test]
1288 fn new_scale_places_factors_on_the_diagonal() {
1289 let t = ComputedTransform3D::new_scale(2.0, -3.0, 0.5);
1290 assert_eq!(t.m[0][0], 2.0);
1291 assert_eq!(t.m[1][1], -3.0);
1292 assert_eq!(t.m[2][2], 0.5);
1293 assert_eq!(t.m[3][3], 1.0);
1294 for r in 0..4 {
1296 for c in 0..4 {
1297 if r != c {
1298 assert_eq!(t.m[r][c], 0.0, "off-diagonal [{r}][{c}]");
1299 }
1300 }
1301 }
1302 }
1303
1304 #[test]
1305 fn new_scale_zero_is_singular_and_inverse_falls_back_to_identity() {
1306 let z = ComputedTransform3D::new_scale(0.0, 0.0, 0.0);
1307 assert_eq!(z.determinant(), 0.0);
1308 assert_eq!(z.inverse(), ComputedTransform3D::IDENTITY);
1311 }
1312
1313 #[test]
1314 fn new_scale_extremes_do_not_panic() {
1315 for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
1316 let t = ComputedTransform3D::new_scale(v, v, v);
1317 assert_eq!(t.m[3][3], 1.0);
1318 assert_eq!(t.m[0][1], 0.0);
1319 }
1320 let nan = ComputedTransform3D::new_scale(f32::NAN, 1.0, 1.0);
1321 assert!(nan.m[0][0].is_nan());
1322 assert!(nan.determinant().is_nan());
1323 }
1324
1325 #[test]
1326 fn new_translation_places_offsets_in_last_row() {
1327 let t = ComputedTransform3D::new_translation(10.0, -20.0, 30.0);
1328 assert_eq!(t.m[3], [10.0, -20.0, 30.0, 1.0]);
1329 assert_eq!(t.m[0], [1.0, 0.0, 0.0, 0.0]);
1331 assert_eq!(t.m[1], [0.0, 1.0, 0.0, 0.0]);
1332 assert_eq!(t.m[2], [0.0, 0.0, 1.0, 0.0]);
1333 }
1334
1335 #[test]
1336 fn new_translation_is_always_invertible_even_at_f32_max() {
1337 let t = ComputedTransform3D::new_translation(f32::MAX, f32::MIN, f32::MAX);
1338 assert_eq!(t.determinant(), 1.0);
1340 let inv = t.inverse();
1341 assert_eq!(inv.m[3][0], -f32::MAX);
1342 assert_eq!(inv.m[3][1], f32::MAX); }
1344
1345 #[test]
1346 fn new_translation_nan_does_not_poison_the_linear_part() {
1347 let t = ComputedTransform3D::new_translation(f32::NAN, f32::INFINITY, 0.0);
1348 assert!(t.m[3][0].is_nan());
1349 assert!(t.m[3][1].is_infinite());
1350 assert_eq!(t.m[0][0], 1.0);
1351 assert!(t.determinant().is_nan() || t.determinant() == 1.0);
1356 }
1357
1358 #[test]
1361 fn new_perspective_finite_distance() {
1362 let t = ComputedTransform3D::new_perspective(100.0);
1363 assert!((t.m[2][3] - (-0.01)).abs() < 1e-6);
1364 assert_eq!(t.m[0][0], 1.0);
1365 assert_eq!(t.m[3][3], 1.0);
1366 }
1367
1368 #[test]
1369 fn new_perspective_zero_distance_divides_by_zero() {
1370 let t = ComputedTransform3D::new_perspective(0.0);
1373 assert!(t.m[2][3].is_infinite() && t.m[2][3].is_sign_negative());
1374 assert!(!all_finite(&t));
1375 }
1376
1377 #[test]
1378 fn new_perspective_extreme_distances_do_not_panic() {
1379 let nan = ComputedTransform3D::new_perspective(f32::NAN);
1380 assert!(nan.m[2][3].is_nan());
1381
1382 let inf = ComputedTransform3D::new_perspective(f32::INFINITY);
1383 assert_eq!(inf.m[2][3], -0.0); assert!(all_finite(&inf));
1385
1386 let tiny = ComputedTransform3D::new_perspective(1e-45);
1388 assert!(tiny.m[2][3].is_infinite() && tiny.m[2][3].is_sign_negative());
1389 }
1390
1391 #[test]
1392 fn new_skew_45_degrees_is_unit_shear() {
1393 let t = ComputedTransform3D::new_skew(45.0, 0.0);
1395 assert!((t.m[1][0] - 1.0).abs() < 1e-5, "tan(45deg) ~= 1, got {}", t.m[1][0]);
1396 assert_eq!(t.m[0][1], 0.0);
1397 assert_eq!(t.m[0][0], 1.0);
1398 assert_eq!(t.m[1][1], 1.0);
1399 assert!((t.determinant() - 1.0).abs() < 1e-4);
1401 }
1402
1403 #[test]
1404 fn new_skew_90_degrees_stays_finite() {
1405 let t = ComputedTransform3D::new_skew(90.0, 90.0);
1409 assert!(all_finite(&t), "skew(90deg) produced a non-finite entry: {t:?}");
1410 assert!(t.m[1][0].abs() > 1e6, "expected a huge shear, got {}", t.m[1][0]);
1411 }
1412
1413 #[test]
1414 fn new_skew_nan_and_infinite_angles_do_not_panic() {
1415 let nan = ComputedTransform3D::new_skew(f32::NAN, 0.0);
1416 assert!(nan.m[1][0].is_nan());
1417 assert_eq!(nan.m[3][3], 1.0);
1418
1419 let inf = ComputedTransform3D::new_skew(f32::INFINITY, f32::NEG_INFINITY);
1421 assert!(inf.m[1][0].is_nan());
1422 assert!(inf.m[0][1].is_nan());
1423 }
1424
1425 #[test]
1428 fn new_rotation_zero_angle_is_identity() {
1429 let t = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, 0.0);
1430 assert_mat_approx(&t, &ComputedTransform3D::IDENTITY, 1e-6);
1431 }
1432
1433 #[test]
1434 fn new_rotation_quarter_turn_about_z() {
1435 let t = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, core::f32::consts::FRAC_PI_2);
1436 assert!((t.m[0][0] - 0.0).abs() < 1e-6);
1438 assert!((t.m[0][1] - 1.0).abs() < 1e-6);
1439 assert!((t.m[1][0] - -1.0).abs() < 1e-6);
1440 assert!((t.m[1][1] - 0.0).abs() < 1e-6);
1441 assert_eq!(t.m[2][2], 1.0);
1442 }
1443
1444 #[test]
1445 fn new_rotation_is_orthonormal_and_det_one() {
1446 let (x, y, z) = (0.267_261_24, 0.534_522_5, 0.801_783_7); let t = ComputedTransform3D::new_rotation(x, y, z, 0.7);
1449 assert!((t.determinant() - 1.0).abs() < 1e-4, "det = {}", t.determinant());
1450 for r in 0..3 {
1451 let len_sq =
1452 t.m[r][0] * t.m[r][0] + t.m[r][1] * t.m[r][1] + t.m[r][2] * t.m[r][2];
1453 assert!((len_sq - 1.0).abs() < 1e-4, "row {r} is not unit length: {len_sq}");
1454 }
1455 assert_mat_approx(&t.inverse(), &t.get_column_major(), 1e-4);
1457 }
1458
1459 #[test]
1460 fn new_rotation_degenerate_zero_axis_yields_identity() {
1461 let t = ComputedTransform3D::new_rotation(0.0, 0.0, 0.0, 1.234);
1465 assert_mat_approx(&t, &ComputedTransform3D::IDENTITY, 1e-6);
1466 }
1467
1468 #[test]
1469 fn new_rotation_nan_and_infinite_theta_do_not_panic() {
1470 let nan = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, f32::NAN);
1471 assert!(nan.m[0][0].is_nan());
1472 assert_eq!(nan.m[3][3], 1.0); let inf = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, f32::INFINITY);
1476 assert!(inf.m[0][0].is_nan());
1477 }
1478
1479 #[test]
1480 fn new_rotation_huge_theta_stays_bounded() {
1481 let t = ComputedTransform3D::new_rotation(0.0, 0.0, 1.0, 1e9);
1483 assert!(all_finite(&t));
1484 for r in 0..3 {
1485 for c in 0..3 {
1486 assert!(t.m[r][c].abs() <= 1.001, "entry [{r}][{c}] = {} escaped [-1,1]", t.m[r][c]);
1487 }
1488 }
1489 }
1490
1491 #[test]
1494 fn determinant_of_identity_is_one() {
1495 assert_eq!(ComputedTransform3D::IDENTITY.determinant(), 1.0);
1496 }
1497
1498 #[test]
1499 fn determinant_of_diagonal_is_product() {
1500 let t = ComputedTransform3D::new(
1501 2.0, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0,
1502 );
1503 assert_eq!(t.determinant(), 120.0);
1504 }
1505
1506 #[test]
1507 fn determinant_matches_independent_cofactor_expansion() {
1508 let t = ComputedTransform3D::new(
1509 3.0, 1.0, 0.0, 2.0, 0.0, 2.0, 1.0, 1.0, 1.0, 0.0, 4.0, 0.0, 2.0, 1.0, 1.0, 3.0,
1510 );
1511 let got = t.determinant();
1512 let want = det4_naive(&t);
1513 assert!((got - want).abs() < 1e-3, "determinant() = {got}, cofactor ref = {want}");
1514 }
1515
1516 #[test]
1517 fn determinant_of_singular_matrices_is_zero() {
1518 assert_eq!(filled(0.0).determinant(), 0.0);
1520 let dup = ComputedTransform3D::new(
1523 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0, 0.0, 1.0, 0.0, 2.0, 4.0, 3.0, 2.0, 1.0,
1524 );
1525 assert!(dup.determinant().abs() < 1e-4, "det = {}", dup.determinant());
1526 assert!(filled(7.0).determinant().abs() < 1e-2);
1528 }
1529
1530 #[test]
1531 fn determinant_overflows_to_infinity_rather_than_wrapping() {
1532 let big = ComputedTransform3D::new_scale(1e20, 1e20, 1e20);
1534 let mut big = big;
1535 big.m[3][3] = 1e20;
1536 let det = big.determinant();
1537 assert!(det.is_infinite() && det.is_sign_positive(), "det = {det}");
1538 }
1539
1540 #[test]
1541 fn determinant_of_nan_matrix_is_nan_not_a_panic() {
1542 assert!(filled(f32::NAN).determinant().is_nan());
1543 }
1544
1545 #[test]
1548 fn inverse_of_identity_is_identity() {
1549 assert_mat_approx(
1550 &ComputedTransform3D::IDENTITY.inverse(),
1551 &ComputedTransform3D::IDENTITY,
1552 1e-6,
1553 );
1554 }
1555
1556 #[test]
1557 fn inverse_round_trips_to_identity() {
1558 let t = ComputedTransform3D::new_translation(10.0, 20.0, 30.0)
1559 .then(&ComputedTransform3D::new_scale(2.0, 4.0, 8.0));
1560 assert_mat_approx(&t.then(&t.inverse()), &ComputedTransform3D::IDENTITY, 1e-4);
1561 assert_mat_approx(&t.inverse().then(&t), &ComputedTransform3D::IDENTITY, 1e-4);
1562 }
1563
1564 #[test]
1565 fn inverse_of_singular_matrix_returns_identity() {
1566 assert_eq!(filled(0.0).inverse(), ComputedTransform3D::IDENTITY);
1567 assert_eq!(
1568 ComputedTransform3D::new_scale(1.0, 1.0, 0.0).inverse(),
1569 ComputedTransform3D::IDENTITY
1570 );
1571 }
1572
1573 #[test]
1574 fn inverse_treats_near_singular_as_singular() {
1575 let tiny = ComputedTransform3D::new_scale(1e-3, 1e-3, 1e-3);
1580 let det = tiny.determinant();
1581 assert!(det > 0.0 && det < f32::EPSILON, "det = {det} (must be a nonzero sub-EPSILON)");
1582 assert_eq!(tiny.inverse(), ComputedTransform3D::IDENTITY);
1583 }
1584
1585 #[test]
1586 fn inverse_of_nan_matrix_does_not_panic() {
1587 let inv = filled(f32::NAN).inverse();
1590 assert!(inv.m.iter().flatten().all(|v| v.is_nan()));
1591 }
1592
1593 #[test]
1594 fn inverse_of_overflowing_matrix_yields_nan_not_a_panic() {
1595 let mut big = ComputedTransform3D::new_scale(1e20, 1e20, 1e20);
1597 big.m[3][3] = 1e20;
1598 let inv = big.inverse();
1599 assert!(inv.m[0][0].is_nan(), "expected NaN from inf * 0.0, got {}", inv.m[0][0]);
1600 }
1601
1602 #[test]
1605 fn multiply_scalar_by_zero_zeroes_every_entry() {
1606 let t = ComputedTransform3D::IDENTITY.multiply_scalar(0.0);
1607 assert!(t.m.iter().flatten().all(|v| *v == 0.0));
1608 }
1609
1610 #[test]
1611 fn multiply_scalar_is_sign_and_magnitude_exact() {
1612 let t = ComputedTransform3D::new_scale(2.0, 3.0, 4.0).multiply_scalar(-1.0);
1613 assert_eq!(t.m[0][0], -2.0);
1614 assert_eq!(t.m[1][1], -3.0);
1615 assert_eq!(t.m[2][2], -4.0);
1616 assert_eq!(t.m[3][3], -1.0);
1617
1618 let m = ComputedTransform3D::IDENTITY.multiply_scalar(f32::MAX);
1619 assert_eq!(m.m[0][0], f32::MAX);
1620 assert_eq!(m.m[0][1], 0.0);
1621 }
1622
1623 #[test]
1624 fn multiply_scalar_overflow_saturates_to_infinity() {
1625 let t = filled(1e30).multiply_scalar(1e30);
1626 assert!(t.m.iter().flatten().all(|v| v.is_infinite() && v.is_sign_positive()));
1627 }
1628
1629 #[test]
1630 fn multiply_scalar_by_infinity_poisons_zero_entries_with_nan() {
1631 let t = ComputedTransform3D::IDENTITY.multiply_scalar(f32::INFINITY);
1635 assert!(t.m[0][0].is_infinite());
1636 assert!(t.m[0][1].is_nan(), "0.0 * inf should be NaN, got {}", t.m[0][1]);
1637 }
1638
1639 #[test]
1640 fn multiply_scalar_by_nan_makes_everything_nan() {
1641 let t = ComputedTransform3D::new_scale(2.0, 3.0, 4.0).multiply_scalar(f32::NAN);
1642 assert!(t.m.iter().flatten().all(|v| v.is_nan()));
1643 }
1644
1645 #[test]
1648 fn get_column_major_transposes() {
1649 let t = ComputedTransform3D::new(
1650 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1651 );
1652 let c = t.get_column_major();
1653 for r in 0..4 {
1654 for col in 0..4 {
1655 assert_eq!(c.m[r][col], t.m[col][r], "transpose slot [{r}][{col}]");
1656 }
1657 }
1658 }
1659
1660 #[test]
1661 fn get_column_major_is_an_involution() {
1662 let t = ComputedTransform3D::new_translation(3.0, -4.0, 5.0)
1663 .then(&ComputedTransform3D::new_scale(2.0, 2.0, 2.0));
1664 assert_eq!(t.get_column_major().get_column_major(), t);
1665 assert_eq!(
1667 ComputedTransform3D::IDENTITY.get_column_major(),
1668 ComputedTransform3D::IDENTITY
1669 );
1670 }
1671
1672 #[test]
1673 fn get_column_major_moves_translation_into_the_last_column() {
1674 let t = ComputedTransform3D::new_translation(7.0, 8.0, 9.0).get_column_major();
1675 assert_eq!(t.m[0][3], 7.0);
1676 assert_eq!(t.m[1][3], 8.0);
1677 assert_eq!(t.m[2][3], 9.0);
1678 assert_eq!(t.m[3], [0.0, 0.0, 0.0, 1.0]);
1679 }
1680
1681 #[test]
1682 fn get_column_major_of_nan_matrix_does_not_panic() {
1683 let t = filled(f32::NAN).get_column_major();
1684 assert!(t.m.iter().flatten().all(|v| v.is_nan()));
1685 }
1686
1687 #[test]
1690 fn transform_point2d_identity_is_the_point_itself() {
1691 let p = LogicalPosition::new(3.0, -4.0);
1692 let out = ComputedTransform3D::IDENTITY.transform_point2d(p).unwrap();
1693 assert_eq!(out.x, 3.0);
1694 assert_eq!(out.y, -4.0);
1695
1696 let zero = ComputedTransform3D::IDENTITY
1697 .transform_point2d(LogicalPosition::zero())
1698 .unwrap();
1699 assert_eq!((zero.x, zero.y), (0.0, 0.0));
1700 }
1701
1702 #[test]
1703 fn transform_point2d_applies_translation_and_scale() {
1704 let t = ComputedTransform3D::new_translation(10.0, 20.0, 0.0);
1705 let out = t.transform_point2d(LogicalPosition::new(1.0, 2.0)).unwrap();
1706 assert_eq!((out.x, out.y), (11.0, 22.0));
1707
1708 let s = ComputedTransform3D::new_scale(2.0, -3.0, 1.0);
1709 let out = s.transform_point2d(LogicalPosition::new(1.5, 2.0)).unwrap();
1710 assert_eq!((out.x, out.y), (3.0, -6.0));
1711 }
1712
1713 #[test]
1714 fn transform_point2d_negative_w_returns_none() {
1715 let mut t = ComputedTransform3D::IDENTITY;
1716 t.m[3][3] = -1.0; assert!(t.transform_point2d(LogicalPosition::new(1.0, 1.0)).is_none());
1718
1719 let mut p = ComputedTransform3D::IDENTITY;
1721 p.m[0][3] = -1.0; assert!(p.transform_point2d(LogicalPosition::new(2.0, 0.0)).is_none());
1723 assert!(p.transform_point2d(LogicalPosition::new(0.5, 0.0)).is_some());
1724 }
1725
1726 #[test]
1727 fn transform_point2d_zero_w_divides_by_zero_instead_of_returning_none() {
1728 let mut t = ComputedTransform3D::IDENTITY;
1733 t.m[3][3] = 0.0;
1734 let out = t.transform_point2d(LogicalPosition::new(1.0, 1.0));
1735 let out = out.expect("w == +0.0 is treated as a valid positive w");
1736 assert!(out.x.is_infinite(), "expected 1.0/0.0 = inf, got {}", out.x);
1737 assert!(out.y.is_infinite());
1738
1739 let mut neg = ComputedTransform3D::IDENTITY;
1743 neg.m[0][3] = -0.0;
1744 neg.m[1][3] = -0.0;
1745 neg.m[3][3] = -0.0;
1746 assert!(neg.transform_point2d(LogicalPosition::new(1.0, 1.0)).is_none());
1747 }
1748
1749 #[test]
1750 fn transform_point2d_nan_matrix_does_not_panic() {
1751 let out = filled(f32::NAN).transform_point2d(LogicalPosition::new(1.0, 1.0));
1752 if let Some(p) = out {
1754 assert!(p.x.is_nan() && p.y.is_nan());
1755 }
1756 }
1757
1758 #[test]
1759 fn transform_point2d_nan_point_does_not_panic() {
1760 let out = ComputedTransform3D::IDENTITY
1761 .transform_point2d(LogicalPosition::new(f32::NAN, f32::NAN));
1762 if let Some(p) = out {
1764 assert!(p.x.is_nan());
1765 }
1766 }
1767
1768 #[test]
1769 fn transform_point2d_extreme_coordinates_saturate() {
1770 let t = ComputedTransform3D::new_translation(10.0, 10.0, 0.0);
1771 let out = t
1772 .transform_point2d(LogicalPosition::new(f32::MAX, f32::MIN))
1773 .unwrap();
1774 assert_eq!(out.x, f32::MAX); assert_eq!(out.y, f32::MIN);
1776
1777 let s = ComputedTransform3D::new_scale(1e30, 1e30, 1.0);
1779 let out = s
1780 .transform_point2d(LogicalPosition::new(1e30, 1e30))
1781 .unwrap();
1782 assert!(out.x.is_infinite() && out.x.is_sign_positive());
1783 }
1784
1785 #[test]
1788 fn scale_for_dpi_touches_only_the_translation_row() {
1789 let mut t = ComputedTransform3D::new(
1790 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1791 );
1792 let before = t;
1793 t.scale_for_dpi(3.0);
1794 assert_eq!(t.m[0], before.m[0]);
1795 assert_eq!(t.m[1], before.m[1]);
1796 assert_eq!(t.m[2], before.m[2]);
1797 assert_eq!(t.m[3][0], 39.0);
1798 assert_eq!(t.m[3][1], 42.0);
1799 assert_eq!(t.m[3][2], 45.0);
1800 assert_eq!(t.m[3][3], 16.0, "m44 must NOT be scaled");
1801 }
1802
1803 #[test]
1804 fn scale_for_dpi_zero_and_negative() {
1805 let mut t = ComputedTransform3D::new_translation(10.0, 20.0, 30.0);
1806 t.scale_for_dpi(0.0);
1807 assert_eq!(t.m[3], [0.0, 0.0, 0.0, 1.0]);
1808
1809 let mut n = ComputedTransform3D::new_translation(10.0, -20.0, 30.0);
1810 n.scale_for_dpi(-2.0);
1811 assert_eq!(n.m[3], [-20.0, 40.0, -60.0, 1.0]);
1812 }
1813
1814 #[test]
1815 fn scale_for_dpi_is_exactly_reversible_for_powers_of_two() {
1816 let original = ComputedTransform3D::new_translation(13.25, -7.5, 0.125);
1817 let mut t = original;
1818 t.scale_for_dpi(2.0);
1819 t.scale_for_dpi(0.5);
1820 assert_eq!(t, original);
1821 }
1822
1823 #[test]
1824 fn scale_for_dpi_overflow_saturates_to_infinity() {
1825 let mut t = ComputedTransform3D::new_translation(1e38, -1e38, 1e38);
1826 t.scale_for_dpi(1e5);
1827 assert!(t.m[3][0].is_infinite() && t.m[3][0].is_sign_positive());
1828 assert!(t.m[3][1].is_infinite() && t.m[3][1].is_sign_negative());
1829 assert_eq!(t.m[3][3], 1.0);
1830 }
1831
1832 #[test]
1833 fn scale_for_dpi_by_infinity_poisons_a_zero_translation() {
1834 let mut t = ComputedTransform3D::IDENTITY;
1837 t.scale_for_dpi(f32::INFINITY);
1838 assert!(t.m[3][0].is_nan());
1839 assert_eq!(t.m[0][0], 1.0, "the linear part must stay untouched");
1840
1841 let mut n = ComputedTransform3D::new_translation(1.0, 2.0, 3.0);
1842 n.scale_for_dpi(f32::INFINITY);
1843 assert!(n.m[3][0].is_infinite());
1844 }
1845
1846 #[test]
1847 fn scale_for_dpi_by_nan_does_not_panic() {
1848 let mut t = ComputedTransform3D::new_translation(1.0, 2.0, 3.0);
1849 t.scale_for_dpi(f32::NAN);
1850 assert!(t.m[3][0].is_nan() && t.m[3][1].is_nan() && t.m[3][2].is_nan());
1851 assert_eq!(t.m[3][3], 1.0);
1852 assert_eq!(t.m[0][0], 1.0);
1853 }
1854
1855 #[test]
1858 fn then_has_identity_as_a_two_sided_unit() {
1859 let a = ComputedTransform3D::new(
1860 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1861 );
1862 assert_mat_approx(&a.then(&ComputedTransform3D::IDENTITY), &a, 1e-4);
1863 assert_mat_approx(&ComputedTransform3D::IDENTITY.then(&a), &a, 1e-4);
1864 }
1865
1866 #[test]
1867 fn then_composes_translations_additively_and_scales_multiplicatively() {
1868 let t = ComputedTransform3D::new_translation(1.0, 2.0, 3.0)
1869 .then(&ComputedTransform3D::new_translation(10.0, 20.0, 30.0));
1870 assert_eq!(t.m[3], [11.0, 22.0, 33.0, 1.0]);
1871
1872 let s = ComputedTransform3D::new_scale(2.0, 3.0, 4.0)
1873 .then(&ComputedTransform3D::new_scale(5.0, 7.0, 11.0));
1874 assert_eq!(s.m[0][0], 10.0);
1875 assert_eq!(s.m[1][1], 21.0);
1876 assert_eq!(s.m[2][2], 44.0);
1877 }
1878
1879 #[test]
1880 fn then_is_associative() {
1881 let a = ComputedTransform3D::new(
1882 1.0, 0.5, 0.0, 0.0, -0.5, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 2.0, 3.0, 0.0, 1.0,
1883 );
1884 let b = ComputedTransform3D::new_scale(2.0, 0.5, 1.0);
1885 let c = ComputedTransform3D::new_translation(-1.0, 4.0, 0.0);
1886 assert_mat_approx(&a.then(&b).then(&c), &a.then(&b.then(&c)), 1e-2);
1887 }
1888
1889 #[test]
1890 fn then_with_extreme_matrices_does_not_panic() {
1891 let big = filled(1e30).then(&filled(1e30));
1892 assert!(big.m[0][0].is_infinite(), "expected overflow to inf, got {}", big.m[0][0]);
1893
1894 let nan = filled(f32::NAN).then(&ComputedTransform3D::IDENTITY);
1895 assert!(nan.m.iter().flatten().all(|v| v.is_nan()));
1896
1897 let mixed = filled(f32::INFINITY).then(&ComputedTransform3D::IDENTITY);
1899 assert!(mixed.m[0][0].is_infinite() || mixed.m[0][0].is_nan());
1900 }
1901
1902 #[cfg(all(target_arch = "x86_64", not(miri)))]
1906 #[test]
1907 fn linear_combine_sse_matches_naive_row_combine() {
1908 if !std::is_x86_feature_detected!("sse") {
1909 return;
1910 }
1911 let b = ComputedTransform3D::new(
1912 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
1913 );
1914 for a in [
1915 [1.0f32, 2.0, 3.0, 4.0],
1916 [0.0, 0.0, 0.0, 0.0],
1917 [-1.5, 0.25, 1e6, -1e6],
1918 ] {
1919 let got = unsafe { ComputedTransform3D::linear_combine_sse(a, &b) };
1921 let want = naive_row_combine(a, &b);
1922 for c in 0..4 {
1923 let tol = 1e-3 * want[c].abs().max(1.0);
1924 assert!((got[c] - want[c]).abs() <= tol, "lane {c}: {} vs {}", got[c], want[c]);
1925 }
1926 }
1927 }
1928
1929 #[cfg(all(target_arch = "x86_64", not(miri)))]
1930 #[test]
1931 fn linear_combine_sse_propagates_nan_per_lane() {
1932 if !std::is_x86_feature_detected!("sse") {
1933 return;
1934 }
1935 let got = unsafe {
1937 ComputedTransform3D::linear_combine_sse(
1938 [f32::NAN; 4],
1939 &ComputedTransform3D::IDENTITY,
1940 )
1941 };
1942 assert!(got.iter().all(|v| v.is_nan()), "NaN must survive the SIMD path: {got:?}");
1943 }
1944
1945 #[cfg(all(target_arch = "x86_64", not(miri)))]
1946 #[test]
1947 fn then_sse_and_then_avx8_agree_with_scalar_then() {
1948 let a = ComputedTransform3D::new(
1949 1.0, 0.5, -2.0, 0.0, 3.0, 1.0, 0.0, 0.25, 0.0, -1.0, 4.0, 0.0, 5.0, 6.0, 7.0, 1.0,
1950 );
1951 let b = ComputedTransform3D::new(
1952 2.0, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, -4.0, 2.0, 0.5, 1.0,
1953 );
1954 let scalar = a.then(&b);
1955
1956 if std::is_x86_feature_detected!("sse") {
1957 let sse = unsafe { a.then_sse(&b) };
1959 assert_mat_approx(&scalar, &sse, 1e-3);
1960 let unit = unsafe { ComputedTransform3D::IDENTITY.then_sse(&b) };
1963 assert_mat_approx(&unit, &b, 1e-4);
1964 }
1965 if std::is_x86_feature_detected!("avx") {
1966 let avx = unsafe { a.then_avx8(&b) };
1968 assert_mat_approx(&scalar, &avx, 1e-3);
1969 let unit = unsafe { ComputedTransform3D::IDENTITY.then_avx8(&b) };
1971 assert_mat_approx(&unit, &b, 1e-4);
1972 }
1973 }
1974
1975 #[cfg(all(target_arch = "x86_64", not(miri)))]
1976 #[test]
1977 fn simd_paths_do_not_panic_on_extreme_matrices() {
1978 let extremes = [filled(f32::NAN), filled(f32::INFINITY), filled(1e30), filled(f32::MIN)];
1983 for a in &extremes {
1984 for b in &extremes {
1985 if std::is_x86_feature_detected!("sse") {
1986 let r = unsafe { a.then_sse(b) };
1988 core::hint::black_box(r);
1989 }
1990 if std::is_x86_feature_detected!("avx") {
1991 let r = unsafe { a.then_avx8(b) };
1993 core::hint::black_box(r);
1994 }
1995 }
1996 }
1997 }
1998
1999 #[cfg(all(target_arch = "x86_64", not(miri)))]
2000 #[test]
2001 fn linear_combine_avx8_computes_two_rows_at_once() {
2002 use core::arch::x86_64::{_mm256_loadu_ps, _mm256_storeu_ps};
2003
2004 if !std::is_x86_feature_detected!("avx") {
2005 return;
2006 }
2007 let b = ComputedTransform3D::new(
2008 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
2009 );
2010 let row0 = [1.0f32, 0.0, -2.0, 3.0];
2011 let row1 = [0.5f32, 4.0, 0.0, -1.0];
2012 let packed: [f32; 8] = [
2013 row0[0], row0[1], row0[2], row0[3], row1[0], row1[1], row1[2], row1[3],
2014 ];
2015 let mut out = [0.0f32; 8];
2016
2017 unsafe {
2020 let a01 = _mm256_loadu_ps(packed.as_ptr());
2021 let res = ComputedTransform3D::linear_combine_avx8(a01, &b);
2022 _mm256_storeu_ps(out.as_mut_ptr(), res);
2023 }
2024
2025 let want0 = naive_row_combine(row0, &b);
2026 let want1 = naive_row_combine(row1, &b);
2027 for c in 0..4 {
2028 assert!((out[c] - want0[c]).abs() < 1e-3, "low lane {c}: {} vs {}", out[c], want0[c]);
2029 assert!(
2030 (out[4 + c] - want1[c]).abs() < 1e-3,
2031 "high lane {c}: {} vs {}",
2032 out[4 + c],
2033 want1[c]
2034 );
2035 }
2036 }
2037
2038 #[test]
2041 fn from_style_transform_vec_empty_is_identity() {
2042 let t = ComputedTransform3D::from_style_transform_vec(
2043 &[],
2044 &StyleTransformOrigin::default(),
2045 100.0,
2046 100.0,
2047 RotationMode::ForWebRender,
2048 );
2049 assert_eq!(t, ComputedTransform3D::IDENTITY);
2050 }
2051
2052 #[test]
2053 fn from_style_transform_vec_accumulates_a_thousand_translations_exactly() {
2054 let list = vec![StyleTransform::TranslateX(PixelValue::const_px(1)); 1000];
2057 let t = ComputedTransform3D::from_style_transform_vec(
2058 &list,
2059 &StyleTransformOrigin::default(),
2060 0.0,
2061 0.0,
2062 RotationMode::ForHitTesting,
2063 );
2064 assert_eq!(t.m[3][0], 1000.0);
2065 assert_eq!(t.m[3][1], 0.0);
2066 assert_eq!(t.m[0][0], 1.0);
2067 }
2068
2069 #[test]
2070 fn from_style_transform_vec_resolves_percentages_against_each_axis() {
2071 let list = vec![
2072 StyleTransform::TranslateX(PixelValue::const_percent(50)),
2073 StyleTransform::TranslateY(PixelValue::const_percent(50)),
2074 ];
2075 let t = ComputedTransform3D::from_style_transform_vec(
2076 &list,
2077 &StyleTransformOrigin::default(),
2078 200.0,
2079 80.0,
2080 RotationMode::ForHitTesting,
2081 );
2082 assert_eq!(t.m[3][0], 100.0); assert_eq!(t.m[3][1], 40.0); }
2085
2086 #[test]
2087 fn from_style_transform_vec_with_extreme_percent_basis_does_not_panic() {
2088 let list = vec![StyleTransform::TranslateX(PixelValue::const_percent(50))];
2089 let run = |basis: f32| {
2090 ComputedTransform3D::from_style_transform_vec(
2091 &list,
2092 &StyleTransformOrigin::default(),
2093 basis,
2094 basis,
2095 RotationMode::ForWebRender,
2096 )
2097 };
2098
2099 for basis in [f32::MAX, f32::MIN, 0.0, -0.0, -1e30] {
2101 let t = run(basis);
2102 assert_eq!(t.m[0][0], 1.0, "linear part corrupted for basis {basis}");
2103 assert!(!t.m[3][0].is_nan(), "finite basis {basis} produced a NaN offset");
2104 }
2105
2106 assert!(run(f32::NAN).m[3][0].is_nan());
2108
2109 let inf = run(f32::INFINITY);
2114 assert!(inf.m[0][0].is_nan(), "0.0 * inf should poison m11, got {}", inf.m[0][0]);
2115 }
2116
2117 #[test]
2118 fn from_style_transform_vec_long_mixed_list_does_not_panic() {
2119 let mut list = vec![];
2120 for i in 0..512 {
2121 list.push(match i % 4 {
2122 0 => StyleTransform::Rotate(AngleValue::const_deg(37)),
2123 1 => StyleTransform::Scale(StyleTransformScale2D {
2124 x: FloatValue::const_new(1),
2125 y: FloatValue::const_new(1),
2126 }),
2127 2 => StyleTransform::SkewX(AngleValue::const_deg(5)),
2128 _ => StyleTransform::TranslateY(PixelValue::const_px(1)),
2129 });
2130 }
2131 let t = ComputedTransform3D::from_style_transform_vec(
2132 &list,
2133 &StyleTransformOrigin::default(),
2134 300.0,
2135 150.0,
2136 RotationMode::ForWebRender,
2137 );
2138 assert!(!t.m[3][3].is_nan());
2140 }
2141
2142 #[test]
2145 fn from_style_transform_matrix_2d_and_3d() {
2146 let m2d = StyleTransform::Matrix(StyleTransformMatrix2D {
2147 a: FloatValue::const_new(2),
2148 b: FloatValue::const_new(3),
2149 c: FloatValue::const_new(4),
2150 d: FloatValue::const_new(5),
2151 tx: FloatValue::const_new(6),
2152 ty: FloatValue::const_new(7),
2153 });
2154 let t = build(&m2d, 0.0, 0.0);
2155 assert_eq!(t.m[0], [2.0, 3.0, 0.0, 0.0]);
2156 assert_eq!(t.m[1], [4.0, 5.0, 0.0, 0.0]);
2157 assert_eq!(t.m[3], [6.0, 7.0, 0.0, 1.0]);
2158
2159 let m3d = StyleTransform::Matrix3D(StyleTransformMatrix3D::default());
2161 assert_eq!(build(&m3d, 0.0, 0.0), ComputedTransform3D::IDENTITY);
2162 }
2163
2164 #[test]
2165 fn from_style_transform_translate_units() {
2166 let t = build(&StyleTransform::TranslateX(PixelValue::const_px(25)), 0.0, 0.0);
2168 assert_eq!(t.m[3][0], 25.0);
2169 let t = build(&StyleTransform::TranslateY(PixelValue::const_em(2)), 0.0, 0.0);
2171 assert_eq!(t.m[3][1], 32.0);
2172 let t = build(
2174 &StyleTransform::Translate(StyleTransformTranslate2D {
2175 x: PixelValue::const_percent(50),
2176 y: PixelValue::const_px(-10),
2177 }),
2178 400.0,
2179 0.0,
2180 );
2181 assert_eq!(t.m[3][0], 200.0);
2182 assert_eq!(t.m[3][1], -10.0);
2183 }
2184
2185 #[test]
2186 fn from_style_transform_translate_z_percent_falls_back_to_the_x_basis() {
2187 let t = build(&StyleTransform::TranslateZ(PixelValue::const_percent(50)), 200.0, 999.0);
2190 assert_eq!(t.m[3][2], 100.0, "translateZ(%) must resolve against the X basis");
2191
2192 let t3d = build(
2193 &StyleTransform::Translate3D(StyleTransformTranslate3D {
2194 x: PixelValue::const_px(0),
2195 y: PixelValue::const_px(0),
2196 z: PixelValue::const_percent(50),
2197 }),
2198 200.0,
2199 999.0,
2200 );
2201 assert_eq!(t3d.m[3][2], 100.0);
2202 }
2203
2204 #[test]
2205 fn from_style_transform_viewport_units_resolve_to_zero() {
2206 let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
2210 let t = build(&StyleTransform::TranslateX(vw), 1000.0, 1000.0);
2211 assert_eq!(t.m[3][0], 0.0);
2212 }
2213
2214 #[test]
2215 fn from_style_transform_saturating_pixel_values_stay_finite() {
2216 let inf = build(&StyleTransform::TranslateX(PixelValue::px(f32::INFINITY)), 0.0, 0.0);
2219 assert!(
2220 inf.m[3][0].is_finite() && inf.m[3][0] > 1e6,
2221 "an infinite px length must saturate, got {}",
2222 inf.m[3][0]
2223 );
2224
2225 let nan = build(&StyleTransform::TranslateX(PixelValue::px(f32::NAN)), 0.0, 0.0);
2226 assert_eq!(nan.m[3][0], 0.0, "NaN px must saturate to 0, not propagate");
2227 }
2228
2229 #[test]
2230 fn from_style_transform_scale_variants() {
2231 let s2d = build(
2232 &StyleTransform::Scale(StyleTransformScale2D {
2233 x: FloatValue::const_new(2),
2234 y: FloatValue::const_new(3),
2235 }),
2236 0.0,
2237 0.0,
2238 );
2239 assert_eq!((s2d.m[0][0], s2d.m[1][1], s2d.m[2][2]), (2.0, 3.0, 1.0));
2240
2241 let s3d = build(
2242 &StyleTransform::Scale3D(StyleTransformScale3D {
2243 x: FloatValue::const_new(2),
2244 y: FloatValue::const_new(3),
2245 z: FloatValue::const_new(4),
2246 }),
2247 0.0,
2248 0.0,
2249 );
2250 assert_eq!((s3d.m[0][0], s3d.m[1][1], s3d.m[2][2]), (2.0, 3.0, 4.0));
2251
2252 let sx = build(&StyleTransform::ScaleX(PercentageValue::const_new(150)), 0.0, 0.0);
2254 assert_eq!((sx.m[0][0], sx.m[1][1], sx.m[2][2]), (1.5, 1.0, 1.0));
2255 let sy = build(&StyleTransform::ScaleY(PercentageValue::const_new(150)), 0.0, 0.0);
2256 assert_eq!((sy.m[0][0], sy.m[1][1], sy.m[2][2]), (1.0, 1.5, 1.0));
2257 let sz = build(&StyleTransform::ScaleZ(PercentageValue::const_new(150)), 0.0, 0.0);
2258 assert_eq!((sz.m[0][0], sz.m[1][1], sz.m[2][2]), (1.0, 1.0, 1.5));
2259 }
2260
2261 #[test]
2262 fn from_style_transform_scale_zero_is_singular() {
2263 let s = build(
2264 &StyleTransform::Scale3D(StyleTransformScale3D {
2265 x: FloatValue::const_new(0),
2266 y: FloatValue::const_new(0),
2267 z: FloatValue::const_new(0),
2268 }),
2269 0.0,
2270 0.0,
2271 );
2272 assert_eq!(s.determinant(), 0.0);
2273 assert_eq!(s.inverse(), ComputedTransform3D::IDENTITY);
2274 let p = s.transform_point2d(LogicalPosition::new(5.0, 9.0)).unwrap();
2276 assert_eq!((p.x, p.y), (0.0, 0.0));
2277 }
2278
2279 #[test]
2280 fn from_style_transform_skew_variants() {
2281 let sx = build(&StyleTransform::SkewX(AngleValue::const_deg(45)), 0.0, 0.0);
2282 assert!((sx.m[1][0] - 1.0).abs() < 1e-5, "skewX => tan(a) at m21");
2283 assert_eq!(sx.m[0][1], 0.0);
2284
2285 let sy = build(&StyleTransform::SkewY(AngleValue::const_deg(45)), 0.0, 0.0);
2286 assert!((sy.m[0][1] - 1.0).abs() < 1e-5, "skewY => tan(b) at m12");
2287 assert_eq!(sy.m[1][0], 0.0);
2288
2289 let sk = build(
2290 &StyleTransform::Skew(StyleTransformSkew2D {
2291 x: AngleValue::const_deg(30),
2292 y: AngleValue::const_deg(60),
2293 }),
2294 0.0,
2295 0.0,
2296 );
2297 assert!((sk.m[1][0] - 0.577_350_3).abs() < 1e-3); assert!((sk.m[0][1] - 1.732_050_8).abs() < 1e-3); }
2300
2301 #[test]
2302 fn from_style_transform_skew_90_degrees_stays_finite() {
2303 let sk = build(&StyleTransform::SkewX(AngleValue::const_deg(90)), 0.0, 0.0);
2304 assert!(all_finite(&sk), "skewX(90deg) must not produce inf/NaN: {sk:?}");
2305 }
2306
2307 #[test]
2308 fn from_style_transform_perspective_zero_is_infinite() {
2309 let p = build(&StyleTransform::Perspective(PixelValue::const_px(0)), 0.0, 0.0);
2310 assert!(p.m[2][3].is_infinite() && p.m[2][3].is_sign_negative());
2312
2313 let ok = build(&StyleTransform::Perspective(PixelValue::const_px(100)), 0.0, 0.0);
2314 assert!((ok.m[2][3] - (-0.01)).abs() < 1e-6);
2315 }
2316
2317 #[test]
2318 fn from_style_transform_rotate_degenerate_axis_is_identity() {
2319 let r = ComputedTransform3D::from_style_transform(
2322 &StyleTransform::Rotate3D(StyleTransformRotate3D {
2323 x: FloatValue::const_new(0),
2324 y: FloatValue::const_new(0),
2325 z: FloatValue::const_new(0),
2326 angle: AngleValue::const_deg(45),
2327 }),
2328 &StyleTransformOrigin::default(), 100.0,
2330 100.0,
2331 RotationMode::ForHitTesting,
2332 );
2333 assert_mat_approx(&r, &ComputedTransform3D::IDENTITY, 1e-4);
2334 }
2335
2336 #[test]
2337 fn from_style_transform_rotate_angle_metrics_agree() {
2338 let deg = build(&StyleTransform::Rotate(AngleValue::const_deg(90)), 0.0, 0.0);
2342 let turn = build(&StyleTransform::RotateZ(AngleValue::turn(0.25)), 0.0, 0.0);
2343 let grad = build(&StyleTransform::Rotate(AngleValue::const_grad(100)), 0.0, 0.0);
2344 assert_mat_approx(°, &turn, 1e-5);
2345 assert_mat_approx(°, &grad, 1e-5);
2346
2347 let rad = build(
2348 &StyleTransform::Rotate(AngleValue::rad(core::f32::consts::FRAC_PI_2)),
2349 0.0,
2350 0.0,
2351 );
2352 assert_mat_approx(°, &rad, 1e-2);
2353 }
2354
2355 #[test]
2356 fn from_style_transform_full_turn_normalizes_to_no_rotation() {
2357 let full = build(&StyleTransform::Rotate(AngleValue::const_deg(720)), 0.0, 0.0);
2359 assert_mat_approx(&full, &ComputedTransform3D::IDENTITY, 1e-3);
2360
2361 let neg = build(&StyleTransform::Rotate(AngleValue::const_deg(-90)), 0.0, 0.0);
2363 let pos = build(&StyleTransform::Rotate(AngleValue::const_deg(270)), 0.0, 0.0);
2364 assert_mat_approx(&neg, &pos, 1e-5);
2365 }
2366
2367 #[test]
2368 fn from_style_transform_rotate_x_y_z_pick_distinct_axes() {
2369 let rx = build(&StyleTransform::RotateX(AngleValue::const_deg(90)), 0.0, 0.0);
2370 let ry = build(&StyleTransform::RotateY(AngleValue::const_deg(90)), 0.0, 0.0);
2371 let rz = build(&StyleTransform::RotateZ(AngleValue::const_deg(90)), 0.0, 0.0);
2372 assert!((rx.m[0][0] - 1.0).abs() < 1e-5);
2375 assert!((ry.m[1][1] - 1.0).abs() < 1e-5);
2376 assert!((rz.m[2][2] - 1.0).abs() < 1e-5);
2377 assert!(rx != ry && ry != rz && rx != rz);
2379 for r in [rx, ry, rz] {
2381 assert!((r.determinant() - 1.0).abs() < 1e-3, "det = {}", r.determinant());
2382 }
2383 }
2384
2385 #[test]
2386 fn from_style_transform_huge_angle_stays_finite() {
2387 let huge = build(&StyleTransform::Rotate(AngleValue::deg(f32::MAX)), 0.0, 0.0);
2390 assert!(all_finite(&huge), "huge angle produced inf/NaN: {huge:?}");
2391 }
2392
2393 #[test]
2396 fn make_rotation_zero_degrees_is_identity_about_any_origin() {
2397 for mode in [RotationMode::ForWebRender, RotationMode::ForHitTesting] {
2398 let r = ComputedTransform3D::make_rotation((10.0, 20.0), 0.0, 0.0, 0.0, 1.0, mode);
2399 assert_mat_approx(&r, &ComputedTransform3D::IDENTITY, 1e-3);
2400 }
2401 }
2402
2403 #[test]
2404 fn make_rotation_modes_are_mutual_inverses() {
2405 let origin = (100.0, 50.0);
2408 let wr =
2409 ComputedTransform3D::make_rotation(origin, 45.0, 0.0, 0.0, 1.0, RotationMode::ForWebRender);
2410 let ht = ComputedTransform3D::make_rotation(
2411 origin,
2412 45.0,
2413 0.0,
2414 0.0,
2415 1.0,
2416 RotationMode::ForHitTesting,
2417 );
2418 assert!(wr != ht, "the two rotation modes must not produce the same matrix");
2419 assert_mat_approx(&wr.then(&ht), &ComputedTransform3D::IDENTITY, 1e-3);
2420 }
2421
2422 #[test]
2423 fn make_rotation_keeps_its_origin_fixed() {
2424 let r = ComputedTransform3D::make_rotation(
2426 (30.0, 40.0),
2427 90.0,
2428 0.0,
2429 0.0,
2430 1.0,
2431 RotationMode::ForHitTesting,
2432 );
2433 let p = r.transform_point2d(LogicalPosition::new(30.0, 40.0)).unwrap();
2434 assert!((p.x - 30.0).abs() < 1e-2, "origin moved in x: {}", p.x);
2435 assert!((p.y - 40.0).abs() < 1e-2, "origin moved in y: {}", p.y);
2436 }
2437
2438 #[test]
2439 fn make_rotation_preserves_volume() {
2440 let r = ComputedTransform3D::make_rotation(
2441 (7.0, -3.0),
2442 123.456,
2443 0.0,
2444 0.0,
2445 1.0,
2446 RotationMode::ForWebRender,
2447 );
2448 assert!((r.determinant() - 1.0).abs() < 1e-3, "det = {}", r.determinant());
2449 }
2450
2451 #[test]
2452 fn make_rotation_nan_degrees_does_not_panic() {
2453 let r = ComputedTransform3D::make_rotation(
2454 (1.0, 2.0),
2455 f32::NAN,
2456 0.0,
2457 0.0,
2458 1.0,
2459 RotationMode::ForWebRender,
2460 );
2461 assert!(r.m[0][0].is_nan(), "NaN degrees must propagate, not panic");
2462 }
2463
2464 #[test]
2465 fn make_rotation_infinite_degrees_does_not_panic() {
2466 for deg in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
2467 let r = ComputedTransform3D::make_rotation(
2468 (0.0, 0.0),
2469 deg,
2470 0.0,
2471 0.0,
2472 1.0,
2473 RotationMode::ForHitTesting,
2474 );
2475 core::hint::black_box(r);
2476 }
2477 }
2478
2479 #[test]
2480 fn make_rotation_extreme_origin_does_not_panic() {
2481 for origin in [
2482 (f32::MAX, f32::MAX),
2483 (f32::INFINITY, f32::NEG_INFINITY),
2484 (f32::NAN, 0.0),
2485 ] {
2486 let r = ComputedTransform3D::make_rotation(
2487 origin,
2488 45.0,
2489 0.0,
2490 0.0,
2491 1.0,
2492 RotationMode::ForWebRender,
2493 );
2494 core::hint::black_box(r);
2495 }
2496 }
2497
2498 #[test]
2499 fn make_rotation_degenerate_axis_is_a_pure_origin_round_trip() {
2500 let r = ComputedTransform3D::make_rotation(
2502 (12.0, 34.0),
2503 90.0,
2504 0.0,
2505 0.0,
2506 0.0,
2507 RotationMode::ForHitTesting,
2508 );
2509 assert_mat_approx(&r, &ComputedTransform3D::IDENTITY, 1e-4);
2510 }
2511}