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 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 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 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 #[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 #[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 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 #[allow(clippy::many_single_char_names)] #[allow(clippy::too_many_lines)] 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 .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 } 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 #[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 #[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 #[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 #[must_use]
581 #[inline]
582 #[allow(clippy::suboptimal_flops)] 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 #[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 #[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 #[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 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
665 self.m[3][0] *= scale_factor;
667 self.m[3][1] *= scale_factor;
668 self.m[3][2] *= scale_factor;
669 }
670
671 #[must_use]
673 #[inline]
674 #[allow(clippy::too_many_lines)] 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 #[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 #[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 #[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 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 #[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 #[must_use]
931 #[inline]
932 #[allow(clippy::suboptimal_flops)] 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 rotation_mode: RotationMode,
941 ) -> Self {
942 degrees = match rotation_mode {
943 RotationMode::ForWebRender => -degrees,
945 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)] mod 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 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 #[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 approx_eq(&ComputedTransform3D::IDENTITY.then(&b), &b);
1016 approx_eq(&a.then(&ComputedTransform3D::IDENTITY), &a);
1017 }
1018
1019 #[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 approx_eq(&ComputedTransform3D::IDENTITY.then(&b), &b);
1048 }
1049
1050 #[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>(); let mut buf = vec![0u8; N * 2 + 16];
1073 let base = buf.as_mut_ptr();
1074
1075 unsafe {
1080 let aligned = base.add(base.align_offset(16));
1081 let misaligned = aligned.add(4); 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)] mod 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 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 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 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 #[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 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 #[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, -1e-45,
1231 f32::MAX,
1232 f32::MIN,
1233 f32::INFINITY,
1234 f32::NEG_INFINITY,
1235 );
1236 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 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 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]); 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 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 #[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 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 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 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 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); }
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 assert!(t.determinant().is_nan() || t.determinant() == 1.0);
1347 }
1348
1349 #[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 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); assert!(all_finite(&inf));
1376
1377 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 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 assert!((t.determinant() - 1.0).abs() < 1e-4);
1392 }
1393
1394 #[test]
1395 fn new_skew_90_degrees_stays_finite() {
1396 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 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 #[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 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 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);
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 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 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); 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 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 #[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 assert_eq!(filled(0.0).determinant(), 0.0);
1511 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 assert!(filled(7.0).determinant().abs() < 1e-2);
1519 }
1520
1521 #[test]
1522 fn determinant_overflows_to_infinity_rather_than_wrapping() {
1523 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 #[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 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 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 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 #[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 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 #[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 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 #[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; assert!(t.transform_point2d(LogicalPosition::new(1.0, 1.0)).is_none());
1709
1710 let mut p = ComputedTransform3D::IDENTITY;
1712 p.m[0][3] = -1.0; 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 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 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 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 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); assert_eq!(out.y, f32::MIN);
1767
1768 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 #[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 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 #[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 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 #[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 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 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 let sse = unsafe { a.then_sse(&b) };
1950 assert_mat_approx(&scalar, &sse, 1e-3);
1951 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 let avx = unsafe { a.then_avx8(&b) };
1959 assert_mat_approx(&scalar, &avx, 1e-3);
1960 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 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 let r = unsafe { a.then_sse(b) };
1979 core::hint::black_box(r);
1980 }
1981 if std::is_x86_feature_detected!("avx") {
1982 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 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 #[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 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); assert_eq!(t.m[3][1], 40.0); }
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 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 assert!(run(f32::NAN).m[3][0].is_nan());
2099
2100 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 assert!(!t.m[3][3].is_nan());
2131 }
2132
2133 #[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 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 let t = build(&StyleTransform::TranslateX(PixelValue::const_px(25)), 0.0, 0.0);
2159 assert_eq!(t.m[3][0], 25.0);
2160 let t = build(&StyleTransform::TranslateY(PixelValue::const_em(2)), 0.0, 0.0);
2162 assert_eq!(t.m[3][1], 32.0);
2163 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 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 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 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 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 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); assert!((sk.m[0][1] - 1.732_050_8).abs() < 1e-3); }
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 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 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(), 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 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(°, &turn, 1e-5);
2336 assert_mat_approx(°, &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(°, &rad, 1e-2);
2344 }
2345
2346 #[test]
2347 fn from_style_transform_full_turn_normalizes_to_no_rotation() {
2348 let full = build(&StyleTransform::Rotate(AngleValue::const_deg(720)), 0.0, 0.0);
2350 assert_mat_approx(&full, &ComputedTransform3D::IDENTITY, 1e-3);
2351
2352 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 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 assert!(rx != ry && ry != rz && rx != rz);
2370 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 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 #[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 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 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 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}