1use alloc::vec::Vec;
22#[cfg(feature = "std")]
23use std::collections::HashMap;
24#[cfg(not(feature = "std"))]
25use alloc::collections::BTreeMap as HashMap;
26use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
27
28use azul_css::props::style::StyleTransformOrigin;
29
30use crate::{
31 dom::{DomId, NodeId},
32 resources::{OpacityKey, TransformKey},
33 styled_dom::StyledDom,
34 transform::{ComputedTransform3D, RotationMode, INITIALIZED, USE_AVX, USE_SSE},
35};
36
37#[derive(Default, Debug, Clone)]
43pub struct GpuValueCache {
44 pub transform_keys: HashMap<NodeId, TransformKey>,
46 pub current_transform_values: HashMap<NodeId, ComputedTransform3D>,
48 pub h_transform_keys: HashMap<NodeId, TransformKey>,
50 pub h_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
52 pub css_transform_keys: HashMap<NodeId, TransformKey>,
55 pub css_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
57 pub opacity_keys: HashMap<NodeId, OpacityKey>,
59 pub current_opacity_values: HashMap<NodeId, f32>,
61 pub scrollbar_v_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
63 pub scrollbar_h_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
65 pub scrollbar_v_opacity_values: HashMap<(DomId, NodeId), f32>,
67 pub scrollbar_h_opacity_values: HashMap<(DomId, NodeId), f32>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
76pub enum GpuTransformKeyEvent {
77 Added(NodeId, TransformKey, ComputedTransform3D),
79 Changed(
81 NodeId,
82 TransformKey,
83 ComputedTransform3D,
84 ComputedTransform3D,
85 ),
86 Removed(NodeId, TransformKey),
88}
89
90impl GpuValueCache {
91 #[must_use] pub fn empty() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
102 pub fn synchronize(&mut self, styled_dom: &StyledDom) -> GpuEventChanges {
103 Self::init_simd_features();
104
105 let transform_key_changes = self.compute_transform_events(styled_dom);
106 self.apply_transform_events(&transform_key_changes);
107
108 let opacity_key_changes = self.compute_opacity_events(styled_dom);
109 self.apply_opacity_events(&opacity_key_changes);
110
111 GpuEventChanges {
112 transform_key_changes,
113 opacity_key_changes,
114 scrollbar_opacity_changes: Vec::new(), }
116 }
117
118 #[allow(clippy::missing_const_for_fn)] fn init_simd_features() {
121 #[cfg(target_arch = "x86_64")]
122 unsafe {
123 if !INITIALIZED.load(AtomicOrdering::SeqCst) {
124 use core::arch::x86_64::__cpuid;
125
126 let mut cpuid = __cpuid(0);
127 let n_ids = cpuid.eax;
128
129 if n_ids > 0 {
130 cpuid = __cpuid(1);
132 USE_SSE.store((cpuid.edx & (1_u32 << 25)) != 0, AtomicOrdering::SeqCst);
133 USE_AVX.store((cpuid.ecx & (1_u32 << 28)) != 0, AtomicOrdering::SeqCst);
134 }
135 INITIALIZED.store(true, AtomicOrdering::SeqCst);
136 }
137 }
138 }
139
140 fn compute_transform_events(&self, styled_dom: &StyledDom) -> Vec<GpuTransformKeyEvent> {
142 let css_property_cache = styled_dom.get_css_property_cache();
143 let node_data = styled_dom.node_data.as_container();
144 let node_states = styled_dom.styled_nodes.as_container();
145
146 let default_transform_origin = StyleTransformOrigin::default();
147
148 let mut events = (0..styled_dom.node_data.len())
155 .filter_map(|node_id| {
156 let node_id = NodeId::new(node_id);
157 let styled_node_state = &node_states[node_id].styled_node_state;
158 if styled_node_state.is_normal() {
160 if let Some(ref cc) = css_property_cache.compact_cache {
161 if !cc.has_transform(node_id.index())
168 && (self.css_current_transform_values.is_empty()
169 || !self.css_current_transform_values.contains_key(&node_id))
170 {
171 return None;
172 }
173 }
174 }
175 let node_data = &node_data[node_id];
176 let transform_prop =
182 css_property_cache.get_transform(node_data, &node_id, styled_node_state);
183 let current_transform = transform_prop
184 .as_ref()
185 .and_then(|v| v.get_property())
186 .map(|t| {
187 let parent_size_width = 0.0;
190 let parent_size_height = 0.0;
191 let transform_origin = css_property_cache.get_transform_origin(
192 node_data,
193 &node_id,
194 styled_node_state,
195 );
196 let transform_origin = transform_origin
197 .as_ref()
198 .and_then(|o| o.get_property())
199 .unwrap_or(&default_transform_origin);
200
201 ComputedTransform3D::from_style_transform_vec(
202 t.as_ref(),
203 transform_origin,
204 parent_size_width,
205 parent_size_height,
206 RotationMode::ForWebRender,
207 )
208 });
209
210 let existing_transform = if self.css_current_transform_values.is_empty() {
211 None
212 } else {
213 self.css_current_transform_values.get(&node_id)
214 };
215
216 match (existing_transform, current_transform) {
217 (None, None) => None, (None, Some(new)) => Some(GpuTransformKeyEvent::Added(
219 node_id,
220 TransformKey::unique(),
221 new,
222 )),
223 (Some(old), Some(new)) => Some(GpuTransformKeyEvent::Changed(
224 node_id,
225 self.css_transform_keys.get(&node_id).copied()?,
226 *old,
227 new,
228 )),
229 (Some(_old), None) => Some(GpuTransformKeyEvent::Removed(
230 node_id,
231 self.css_transform_keys.get(&node_id).copied()?,
232 )),
233 }
234 })
235 .collect::<Vec<GpuTransformKeyEvent>>();
236
237 let node_count = styled_dom.node_data.len();
241 for (node_id, key) in &self.css_transform_keys {
242 if node_id.index() >= node_count {
243 events.push(GpuTransformKeyEvent::Removed(*node_id, *key));
244 }
245 }
246
247 events
248 }
249
250 fn apply_transform_events(&mut self, events: &[GpuTransformKeyEvent]) {
252 for event in events {
254 match &event {
255 GpuTransformKeyEvent::Added(node_id, key, matrix) => {
256 self.css_transform_keys.insert(*node_id, *key);
257 self.css_current_transform_values.insert(*node_id, *matrix);
258 }
259 GpuTransformKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
260 self.css_current_transform_values.insert(*node_id, *new_state);
261 }
262 GpuTransformKeyEvent::Removed(node_id, _key) => {
263 self.css_transform_keys.remove(node_id);
264 self.css_current_transform_values.remove(node_id);
265 }
266 }
267 }
268 }
269
270 fn compute_opacity_events(&self, styled_dom: &StyledDom) -> Vec<GpuOpacityKeyEvent> {
272 let css_property_cache = styled_dom.get_css_property_cache();
273 let node_data = styled_dom.node_data.as_container();
274 let node_states = styled_dom.styled_nodes.as_container();
275
276 let mut events = (0..styled_dom.node_data.len())
283 .filter_map(|node_id| {
284 let node_id = NodeId::new(node_id);
285 let styled_node_state = &node_states[node_id].styled_node_state;
286
287 let mut compact_opacity: Option<f32> = None;
289 if styled_node_state.is_normal() {
290 if let Some(ref cc) = css_property_cache.compact_cache {
291 let raw = cc.get_opacity_raw(node_id.index());
292 compact_opacity = if raw == azul_css::compact_cache::OPACITY_SENTINEL {
293 self.current_opacity_values.get(&node_id)?;
295 None
296 } else {
297 Some(f32::from(raw) / 254.0)
298 };
299 }
300 }
301
302 let node_data = &node_data[node_id];
303 let current_opacity: Option<f32> = if let Some(v) = compact_opacity {
304 Some(v)
306 } else if styled_node_state.is_normal() && css_property_cache.compact_cache.is_some() {
307 None
309 } else {
310 css_property_cache
311 .get_opacity(node_data, &node_id, styled_node_state)?
312 .get_property()
313 .map(|p| p.inner.normalized())
314 };
315 let existing_opacity = self.current_opacity_values.get(&node_id);
316
317 match (existing_opacity, current_opacity) {
318 (None, None) => None, (None, Some(new)) => Some(GpuOpacityKeyEvent::Added(
320 node_id,
321 OpacityKey::unique(),
322 new,
323 )),
324 (Some(old), Some(new)) => Some(GpuOpacityKeyEvent::Changed(
325 node_id,
326 self.opacity_keys.get(&node_id).copied()?,
327 *old,
328 new,
329 )),
330 (Some(_old), None) => Some(GpuOpacityKeyEvent::Removed(
331 node_id,
332 self.opacity_keys.get(&node_id).copied()?,
333 )),
334 }
335 })
336 .collect::<Vec<GpuOpacityKeyEvent>>();
337
338 let node_count = styled_dom.node_data.len();
341 for (node_id, key) in &self.opacity_keys {
342 if node_id.index() >= node_count {
343 events.push(GpuOpacityKeyEvent::Removed(*node_id, *key));
344 }
345 }
346
347 events
348 }
349
350 fn apply_opacity_events(&mut self, events: &[GpuOpacityKeyEvent]) {
352 for event in events {
354 match &event {
355 GpuOpacityKeyEvent::Added(node_id, key, opacity) => {
356 self.opacity_keys.insert(*node_id, *key);
357 self.current_opacity_values.insert(*node_id, *opacity);
358 }
359 GpuOpacityKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
360 self.current_opacity_values.insert(*node_id, *new_state);
361 }
362 GpuOpacityKeyEvent::Removed(node_id, _key) => {
363 self.opacity_keys.remove(node_id);
364 self.current_opacity_values.remove(node_id);
365 }
366 }
367 }
368 }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
376pub enum GpuScrollbarOpacityEvent {
377 VerticalAdded(DomId, NodeId, OpacityKey, f32),
379 VerticalChanged(DomId, NodeId, OpacityKey, f32, f32),
381 VerticalRemoved(DomId, NodeId, OpacityKey),
383 HorizontalAdded(DomId, NodeId, OpacityKey, f32),
385 HorizontalChanged(DomId, NodeId, OpacityKey, f32, f32),
387 HorizontalRemoved(DomId, NodeId, OpacityKey),
389}
390
391#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
396pub struct GpuEventChanges {
397 pub transform_key_changes: Vec<GpuTransformKeyEvent>,
399 pub opacity_key_changes: Vec<GpuOpacityKeyEvent>,
401 pub scrollbar_opacity_changes: Vec<GpuScrollbarOpacityEvent>,
403}
404
405impl GpuEventChanges {
406 #[must_use] pub fn empty() -> Self {
408 Self::default()
409 }
410
411 #[must_use] pub const fn is_empty(&self) -> bool {
413 self.transform_key_changes.is_empty()
414 && self.opacity_key_changes.is_empty()
415 && self.scrollbar_opacity_changes.is_empty()
416 }
417
418 pub fn merge(&mut self, other: &mut Self) {
422 self.transform_key_changes.append(&mut other.transform_key_changes);
423 self.opacity_key_changes.append(&mut other.opacity_key_changes);
424 self.scrollbar_opacity_changes.append(&mut other.scrollbar_opacity_changes);
425 }
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
433pub enum GpuOpacityKeyEvent {
434 Added(NodeId, OpacityKey, f32),
436 Changed(NodeId, OpacityKey, f32, f32),
438 Removed(NodeId, OpacityKey),
440}
441
442#[cfg(test)]
443#[allow(clippy::float_cmp)] mod autotest_generated {
445 use azul_css::css::Css;
446
447 use super::*;
448 use crate::dom::Dom;
449
450 fn plain_styled_dom() -> StyledDom {
453 let mut dom = Dom::create_body();
454 StyledDom::create(&mut dom, Css::empty())
455 }
456
457 fn styled_dom_from_css(css_src: &str, class: &str) -> StyledDom {
459 let css = azul_css::parser2::new_from_str(css_src).0;
460 let mut dom = Dom::create_body()
461 .with_children(vec![Dom::create_div().with_class(class.to_string().into())].into());
462 StyledDom::create(&mut dom, css)
463 }
464
465 fn hostile_matrix() -> ComputedTransform3D {
467 ComputedTransform3D::new(
468 f32::NAN,
469 f32::INFINITY,
470 f32::NEG_INFINITY,
471 f32::MIN,
472 f32::MAX,
473 0.0,
474 -0.0,
475 f32::EPSILON,
476 1.0,
477 2.0,
478 3.0,
479 4.0,
480 5.0,
481 6.0,
482 7.0,
483 8.0,
484 )
485 }
486
487 fn scale_matrix(s: f32) -> ComputedTransform3D {
488 ComputedTransform3D::new(
489 s, 0.0, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
490 )
491 }
492
493 #[test]
498 fn gpu_value_cache_empty_holds_no_keys_and_no_values() {
499 let cache = GpuValueCache::empty();
500 assert!(cache.transform_keys.is_empty());
501 assert!(cache.current_transform_values.is_empty());
502 assert!(cache.h_transform_keys.is_empty());
503 assert!(cache.h_current_transform_values.is_empty());
504 assert!(cache.css_transform_keys.is_empty());
505 assert!(cache.css_current_transform_values.is_empty());
506 assert!(cache.opacity_keys.is_empty());
507 assert!(cache.current_opacity_values.is_empty());
508 assert!(cache.scrollbar_v_opacity_keys.is_empty());
509 assert!(cache.scrollbar_h_opacity_keys.is_empty());
510 assert!(cache.scrollbar_v_opacity_values.is_empty());
511 assert!(cache.scrollbar_h_opacity_values.is_empty());
512 }
513
514 #[test]
515 fn gpu_event_changes_empty_equals_default_and_is_empty() {
516 let changes = GpuEventChanges::empty();
517 assert_eq!(changes, GpuEventChanges::default());
518 assert!(changes.is_empty());
519 assert_eq!(changes.transform_key_changes.len(), 0);
520 assert_eq!(changes.opacity_key_changes.len(), 0);
521 assert_eq!(changes.scrollbar_opacity_changes.len(), 0);
522 }
523
524 #[test]
529 fn is_empty_is_false_when_any_single_event_vec_is_populated() {
530 let node = NodeId::ZERO;
531
532 let mut only_transform = GpuEventChanges::empty();
533 only_transform
534 .transform_key_changes
535 .push(GpuTransformKeyEvent::Removed(node, TransformKey::unique()));
536 assert!(!only_transform.is_empty());
537
538 let mut only_opacity = GpuEventChanges::empty();
539 only_opacity
540 .opacity_key_changes
541 .push(GpuOpacityKeyEvent::Removed(node, OpacityKey::unique()));
542 assert!(!only_opacity.is_empty());
543
544 let mut only_scrollbar = GpuEventChanges::empty();
547 only_scrollbar
548 .scrollbar_opacity_changes
549 .push(GpuScrollbarOpacityEvent::VerticalRemoved(
550 DomId::ROOT_ID,
551 node,
552 OpacityKey::unique(),
553 ));
554 assert!(!only_scrollbar.is_empty());
555 }
556
557 #[test]
562 fn merge_moves_every_event_and_drains_the_source() {
563 let node = NodeId::new(7);
564
565 let mut target = GpuEventChanges::empty();
566 target.transform_key_changes.push(GpuTransformKeyEvent::Added(
567 node,
568 TransformKey::unique(),
569 ComputedTransform3D::IDENTITY,
570 ));
571
572 let mut source = GpuEventChanges::empty();
573 source
574 .transform_key_changes
575 .push(GpuTransformKeyEvent::Removed(node, TransformKey::unique()));
576 source
577 .opacity_key_changes
578 .push(GpuOpacityKeyEvent::Added(node, OpacityKey::unique(), 0.25));
579 source
580 .scrollbar_opacity_changes
581 .push(GpuScrollbarOpacityEvent::HorizontalAdded(
582 DomId::ROOT_ID,
583 node,
584 OpacityKey::unique(),
585 1.0,
586 ));
587
588 target.merge(&mut source);
589
590 assert!(source.is_empty(), "merge must consume the source");
591 assert_eq!(target.transform_key_changes.len(), 2);
592 assert_eq!(target.opacity_key_changes.len(), 1);
593 assert_eq!(target.scrollbar_opacity_changes.len(), 1);
594 assert!(matches!(
596 target.transform_key_changes[0],
597 GpuTransformKeyEvent::Added(..)
598 ));
599 assert!(matches!(
600 target.transform_key_changes[1],
601 GpuTransformKeyEvent::Removed(..)
602 ));
603 }
604
605 #[test]
606 fn merge_with_an_empty_set_is_the_identity_in_both_directions() {
607 let node = NodeId::new(1);
608 let mut populated = GpuEventChanges::empty();
609 populated
610 .opacity_key_changes
611 .push(GpuOpacityKeyEvent::Added(node, OpacityKey::unique(), 0.5));
612 let snapshot = populated.clone();
613
614 let mut empty = GpuEventChanges::empty();
616 populated.merge(&mut empty);
617 assert_eq!(populated, snapshot);
618 assert!(empty.is_empty());
619
620 let mut target = GpuEventChanges::empty();
622 let mut source = snapshot.clone();
623 target.merge(&mut source);
624 assert_eq!(target, snapshot);
625 assert!(source.is_empty());
626 }
627
628 #[test]
629 fn merging_large_event_vectors_does_not_panic_and_is_idempotent_when_drained() {
630 let mut target = GpuEventChanges::empty();
631 let mut source = GpuEventChanges::empty();
632 for i in 0..10_000usize {
633 target
634 .opacity_key_changes
635 .push(GpuOpacityKeyEvent::Removed(NodeId::new(i), OpacityKey::unique()));
636 source
637 .opacity_key_changes
638 .push(GpuOpacityKeyEvent::Removed(NodeId::new(i), OpacityKey::unique()));
639 }
640
641 target.merge(&mut source);
642 assert_eq!(target.opacity_key_changes.len(), 20_000);
643 assert!(source.is_empty());
644
645 target.merge(&mut source);
647 assert_eq!(target.opacity_key_changes.len(), 20_000);
648 }
649
650 #[test]
655 fn apply_transform_events_on_an_empty_slice_is_a_noop() {
656 let mut cache = GpuValueCache::empty();
657 cache.apply_transform_events(&[]);
658 assert!(cache.css_transform_keys.is_empty());
659 assert!(cache.css_current_transform_values.is_empty());
660 }
661
662 #[test]
663 fn apply_transform_events_keeps_keys_and_values_in_lockstep() {
664 let node = NodeId::new(3);
665 let key = TransformKey::unique();
666 let mut cache = GpuValueCache::empty();
667
668 cache.apply_transform_events(&[GpuTransformKeyEvent::Added(
669 node,
670 key,
671 ComputedTransform3D::IDENTITY,
672 )]);
673 assert_eq!(cache.css_transform_keys.get(&node), Some(&key));
674 assert_eq!(
675 cache.css_current_transform_values.get(&node),
676 Some(&ComputedTransform3D::IDENTITY)
677 );
678
679 let scaled = scale_matrix(2.0);
682 cache.apply_transform_events(&[GpuTransformKeyEvent::Changed(
683 node,
684 key,
685 ComputedTransform3D::IDENTITY,
686 scaled,
687 )]);
688 assert_eq!(cache.css_transform_keys.get(&node), Some(&key));
689 assert_eq!(cache.css_current_transform_values.get(&node), Some(&scaled));
690
691 cache.apply_transform_events(&[GpuTransformKeyEvent::Removed(node, key)]);
692 assert!(cache.css_transform_keys.is_empty());
693 assert!(cache.css_current_transform_values.is_empty());
694 }
695
696 #[test]
697 fn removing_an_uncached_or_out_of_range_node_does_not_panic() {
698 let mut cache = GpuValueCache::empty();
699 cache.apply_transform_events(&[
702 GpuTransformKeyEvent::Removed(NodeId::new(usize::MAX), TransformKey::unique()),
703 GpuTransformKeyEvent::Removed(NodeId::ZERO, TransformKey::unique()),
704 ]);
705 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(
706 NodeId::new(usize::MAX),
707 OpacityKey::unique(),
708 )]);
709 assert!(cache.css_transform_keys.is_empty());
710 assert!(cache.css_current_transform_values.is_empty());
711 assert!(cache.opacity_keys.is_empty());
712 assert!(cache.current_opacity_values.is_empty());
713 }
714
715 #[test]
716 fn transform_events_are_applied_in_slice_order_within_one_batch() {
717 let node = NodeId::new(2);
718 let first = TransformKey::unique();
719 let second = TransformKey::unique();
720
721 let mut cache = GpuValueCache::empty();
723 cache.apply_transform_events(&[
724 GpuTransformKeyEvent::Added(node, first, ComputedTransform3D::IDENTITY),
725 GpuTransformKeyEvent::Removed(node, first),
726 ]);
727 assert!(cache.css_transform_keys.is_empty());
728 assert!(cache.css_current_transform_values.is_empty());
729
730 let mut cache = GpuValueCache::empty();
732 cache.apply_transform_events(&[
733 GpuTransformKeyEvent::Removed(node, first),
734 GpuTransformKeyEvent::Added(node, second, ComputedTransform3D::IDENTITY),
735 ]);
736 assert_eq!(cache.css_transform_keys.get(&node), Some(&second));
737 }
738
739 #[test]
740 fn two_added_events_for_one_node_keep_only_the_last_key() {
741 let node = NodeId::ZERO;
742 let first = TransformKey::unique();
743 let second = TransformKey::unique();
744 assert_ne!(
745 first, second,
746 "TransformKey::unique() must never hand out the same id twice"
747 );
748
749 let mut cache = GpuValueCache::empty();
750 cache.apply_transform_events(&[
751 GpuTransformKeyEvent::Added(node, first, ComputedTransform3D::IDENTITY),
752 GpuTransformKeyEvent::Added(node, second, scale_matrix(3.0)),
753 ]);
754 assert_eq!(cache.css_transform_keys.len(), 1);
755 assert_eq!(cache.css_transform_keys.get(&node), Some(&second));
756 assert_eq!(cache.css_current_transform_values.get(&node), Some(&scale_matrix(3.0)));
757 }
758
759 #[test]
760 fn a_changed_event_for_an_uncached_node_inserts_a_value_but_no_key() {
761 let node = NodeId::new(5);
764 let mut cache = GpuValueCache::empty();
765 cache.apply_transform_events(&[GpuTransformKeyEvent::Changed(
766 node,
767 TransformKey::unique(),
768 ComputedTransform3D::IDENTITY,
769 scale_matrix(2.0),
770 )]);
771 assert_eq!(cache.css_current_transform_values.get(&node), Some(&scale_matrix(2.0)));
772 assert!(
773 cache.css_transform_keys.is_empty(),
774 "Changed only writes the value map; the key map stays untouched"
775 );
776 }
777
778 #[test]
779 fn non_finite_matrices_are_stored_verbatim_and_never_compare_equal() {
780 let node = NodeId::new(1);
781 let mut cache = GpuValueCache::empty();
782 cache.apply_transform_events(&[GpuTransformKeyEvent::Added(
783 node,
784 TransformKey::unique(),
785 hostile_matrix(),
786 )]);
787
788 let stored = cache
789 .css_current_transform_values
790 .get(&node)
791 .copied()
792 .expect("the matrix must be cached even when it is full of NaN/Inf");
793 assert!(stored.m[0][0].is_nan());
794 assert!(stored.m[0][1].is_infinite() && stored.m[0][1].is_sign_positive());
795 assert!(stored.m[0][2].is_infinite() && stored.m[0][2].is_sign_negative());
796 assert_eq!(stored.m[0][3], f32::MIN);
797 assert_eq!(stored.m[1][0], f32::MAX);
798
799 let a = hostile_matrix();
802 let b = hostile_matrix();
803 assert_ne!(a, b);
804 }
805
806 #[test]
811 fn apply_opacity_events_add_change_remove_round_trip() {
812 let node = NodeId::new(4);
813 let key = OpacityKey::unique();
814 let mut cache = GpuValueCache::empty();
815
816 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Added(node, key, 0.25)]);
817 assert_eq!(cache.opacity_keys.get(&node), Some(&key));
818 assert_eq!(cache.current_opacity_values.get(&node), Some(&0.25));
819
820 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Changed(node, key, 0.25, 0.75)]);
821 assert_eq!(cache.opacity_keys.get(&node), Some(&key));
822 assert_eq!(cache.current_opacity_values.get(&node), Some(&0.75));
823
824 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(node, key)]);
825 assert!(cache.opacity_keys.is_empty());
826 assert!(cache.current_opacity_values.is_empty());
827
828 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(node, key)]);
830 assert!(cache.opacity_keys.is_empty());
831 }
832
833 #[test]
834 fn apply_opacity_events_stores_out_of_range_values_verbatim() {
835 let cases = [
838 f32::NAN,
839 f32::INFINITY,
840 f32::NEG_INFINITY,
841 -1.0,
842 2.0,
843 1e30,
844 -0.0,
845 ];
846 let mut cache = GpuValueCache::empty();
847 for (i, value) in cases.iter().enumerate() {
848 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Added(
849 NodeId::new(i),
850 OpacityKey::unique(),
851 *value,
852 )]);
853 }
854
855 assert_eq!(cache.current_opacity_values.len(), cases.len());
856 assert_eq!(cache.opacity_keys.len(), cache.current_opacity_values.len());
857 assert!(cache.current_opacity_values[&NodeId::new(0)].is_nan());
858 assert!(cache.current_opacity_values[&NodeId::new(1)].is_infinite());
859 assert_eq!(cache.current_opacity_values[&NodeId::new(3)], -1.0);
860 assert_eq!(cache.current_opacity_values[&NodeId::new(4)], 2.0);
861 assert_eq!(cache.current_opacity_values[&NodeId::new(5)], 1e30);
862 }
863
864 #[test]
869 fn init_simd_features_is_idempotent() {
870 GpuValueCache::init_simd_features();
871 GpuValueCache::init_simd_features();
872
873 #[cfg(target_arch = "x86_64")]
874 {
875 assert!(
876 INITIALIZED.load(AtomicOrdering::SeqCst),
877 "the one-time init flag must be set after the first call"
878 );
879 let sse = USE_SSE.load(AtomicOrdering::SeqCst);
880 let avx = USE_AVX.load(AtomicOrdering::SeqCst);
881 GpuValueCache::init_simd_features();
882 assert_eq!(sse, USE_SSE.load(AtomicOrdering::SeqCst));
883 assert_eq!(avx, USE_AVX.load(AtomicOrdering::SeqCst));
884 }
885 }
886
887 #[test]
892 fn synchronize_on_a_transform_and_opacity_free_dom_emits_nothing() {
893 let styled = plain_styled_dom();
894 let mut cache = GpuValueCache::empty();
895
896 let changes = cache.synchronize(&styled);
897
898 assert!(changes.is_empty());
899 assert!(cache.css_transform_keys.is_empty());
900 assert!(cache.opacity_keys.is_empty());
901 }
902
903 #[test]
904 fn synchronize_never_fills_scrollbar_opacity_changes() {
905 let styled = plain_styled_dom();
908 let mut cache = GpuValueCache::empty();
909 cache
910 .scrollbar_v_opacity_keys
911 .insert((DomId::ROOT_ID, NodeId::ZERO), OpacityKey::unique());
912 cache
913 .scrollbar_v_opacity_values
914 .insert((DomId::ROOT_ID, NodeId::ZERO), 0.5);
915
916 let changes = cache.synchronize(&styled);
917
918 assert!(changes.scrollbar_opacity_changes.is_empty());
919 assert_eq!(cache.scrollbar_v_opacity_keys.len(), 1);
920 assert_eq!(cache.scrollbar_v_opacity_values.len(), 1);
921 }
922
923 #[test]
924 fn opacity_round_trips_from_css_through_the_compact_cache_quantizer() {
925 const STEP: f32 = 1.0 / 254.0;
929 for (css_value, expected) in [
930 ("0", 0.0f32),
931 ("0.25", 0.25),
932 ("0.5", 0.5),
933 ("1", 1.0),
934 ("50%", 0.5),
935 ("100%", 1.0),
936 ] {
937 let styled = styled_dom_from_css(&format!(".fade {{ opacity: {css_value}; }}"), "fade");
938 let mut cache = GpuValueCache::empty();
939
940 let changes = cache.synchronize(&styled);
941
942 let decoded = changes
943 .opacity_key_changes
944 .iter()
945 .find_map(|e| match e {
946 GpuOpacityKeyEvent::Added(_, _, v) => Some(*v),
947 _ => None,
948 })
949 .unwrap_or_else(|| panic!("`opacity: {css_value}` produced no Added event"));
950
951 assert!(
952 (decoded - expected).abs() <= STEP,
953 "`opacity: {css_value}` decoded as {decoded}, expected ~{expected}"
954 );
955 assert!(
956 (0.0..=1.0).contains(&decoded),
957 "`opacity: {css_value}` decoded to {decoded}, outside [0, 1]"
958 );
959 }
960 }
961
962 #[test]
963 fn re_synchronizing_an_unchanged_dom_never_mints_a_second_key() {
964 let styled = styled_dom_from_css(".fade { opacity: 0.5; }", "fade");
965 let mut cache = GpuValueCache::empty();
966
967 let first = cache.synchronize(&styled);
968 let added_first = first
969 .opacity_key_changes
970 .iter()
971 .filter(|e| matches!(e, GpuOpacityKeyEvent::Added(..)))
972 .count();
973 assert_eq!(added_first, 1, "the .fade div must get exactly one opacity key");
974 let keys_after_first = cache.opacity_keys.clone();
975
976 let second = cache.synchronize(&styled);
977
978 assert!(
979 !second
980 .opacity_key_changes
981 .iter()
982 .any(|e| matches!(e, GpuOpacityKeyEvent::Added(..))),
983 "re-syncing an unchanged DOM must not allocate a new OpacityKey (GPU key leak)"
984 );
985 assert_eq!(
986 cache.opacity_keys, keys_after_first,
987 "the OpacityKey of a node must be stable across syncs"
988 );
989 }
990
991 #[test]
992 fn synchronize_evicts_cached_keys_for_nodes_that_no_longer_exist() {
993 let styled = plain_styled_dom();
996 assert_eq!(styled.node_data.len(), 1);
997
998 let ghost_a = NodeId::new(9_999);
999 let ghost_b = NodeId::new(usize::MAX); let mut cache = GpuValueCache::empty();
1001 for ghost in [ghost_a, ghost_b] {
1002 cache.css_transform_keys.insert(ghost, TransformKey::unique());
1003 cache
1004 .css_current_transform_values
1005 .insert(ghost, ComputedTransform3D::IDENTITY);
1006 cache.opacity_keys.insert(ghost, OpacityKey::unique());
1007 cache.current_opacity_values.insert(ghost, 0.5);
1008 }
1009
1010 let changes = cache.synchronize(&styled);
1011
1012 assert_eq!(changes.transform_key_changes.len(), 2);
1013 assert_eq!(changes.opacity_key_changes.len(), 2);
1014 assert!(changes
1015 .transform_key_changes
1016 .iter()
1017 .all(|e| matches!(e, GpuTransformKeyEvent::Removed(..))));
1018 assert!(changes
1019 .opacity_key_changes
1020 .iter()
1021 .all(|e| matches!(e, GpuOpacityKeyEvent::Removed(..))));
1022 assert!(cache.css_transform_keys.is_empty());
1023 assert!(cache.css_current_transform_values.is_empty());
1024 assert!(cache.opacity_keys.is_empty());
1025 assert!(cache.current_opacity_values.is_empty());
1026 }
1027
1028 #[test]
1029 fn a_cached_opacity_is_evicted_when_the_node_loses_its_opacity() {
1030 let styled = plain_styled_dom();
1033 let node = NodeId::ZERO;
1034 let key = OpacityKey::unique();
1035
1036 let mut cache = GpuValueCache::empty();
1037 cache.opacity_keys.insert(node, key);
1038 cache.current_opacity_values.insert(node, 0.5);
1039
1040 let changes = cache.synchronize(&styled);
1041
1042 assert_eq!(
1043 changes.opacity_key_changes,
1044 vec![GpuOpacityKeyEvent::Removed(node, key)]
1045 );
1046 assert!(cache.opacity_keys.is_empty());
1047 assert!(cache.current_opacity_values.is_empty());
1048 }
1049
1050 #[test]
1051 fn a_cached_transform_is_evicted_when_the_node_loses_its_transform() {
1052 let styled = plain_styled_dom();
1058 let node = NodeId::ZERO;
1059 let key = TransformKey::unique();
1060
1061 let mut cache = GpuValueCache::empty();
1062 cache.css_transform_keys.insert(node, key);
1063 cache
1064 .css_current_transform_values
1065 .insert(node, ComputedTransform3D::IDENTITY);
1066
1067 let changes = cache.synchronize(&styled);
1068
1069 assert_eq!(
1070 changes.transform_key_changes,
1071 vec![GpuTransformKeyEvent::Removed(node, key)]
1072 );
1073 assert!(
1074 cache.css_transform_keys.is_empty(),
1075 "a stale CSS transform key was not evicted"
1076 );
1077 assert!(
1078 cache.css_current_transform_values.is_empty(),
1079 "a stale CSS transform value was not evicted"
1080 );
1081 }
1082
1083 #[test]
1084 fn synchronize_survives_malformed_and_unicode_css() {
1085 for css_src in [
1086 "",
1087 ".fade { opacity: ; }",
1088 ".fade { opacity: 🦀; }",
1089 ".fade { opacity: -1; }",
1090 ".fade { opacity: 99999999999; }",
1091 ".fade { opacity: NaN; }",
1092 ".fade { transform: }",
1093 ".fade { transform: rotate(NaN); }",
1094 ".fade { transform: rotate(1e400deg); }",
1095 ".fade { transform: rotate(); }",
1096 ] {
1097 let styled = styled_dom_from_css(css_src, "fade");
1098 let mut cache = GpuValueCache::empty();
1099
1100 let _ = cache.synchronize(&styled);
1101
1102 assert_eq!(
1104 cache.css_transform_keys.len(),
1105 cache.css_current_transform_values.len(),
1106 "transform key/value maps drifted apart (css: {css_src:?})"
1107 );
1108 assert_eq!(
1109 cache.opacity_keys.len(),
1110 cache.current_opacity_values.len(),
1111 "opacity key/value maps drifted apart (css: {css_src:?})"
1112 );
1113 for value in cache.current_opacity_values.values() {
1114 assert!(
1115 !value.is_nan(),
1116 "a NaN opacity reached the GPU cache (css: {css_src:?})"
1117 );
1118 assert!(
1119 (0.0..=1.0).contains(value),
1120 "an out-of-range opacity ({value}) reached the GPU cache (css: {css_src:?})"
1121 );
1122 }
1123 }
1124 }
1125
1126 #[test]
1127 fn synchronize_on_a_500_node_dom_keeps_every_key_in_range() {
1128 let css = azul_css::parser2::new_from_str(
1129 ".t { transform: rotate(45deg); } .o { opacity: 0.25; }",
1130 )
1131 .0;
1132 let children = (0..500usize)
1133 .map(|i| {
1134 let class = if i % 2 == 0 { "t" } else { "o" };
1135 Dom::create_div().with_class(class.to_string().into())
1136 })
1137 .collect::<Vec<Dom>>();
1138 let mut dom = Dom::create_body().with_children(children.into());
1139 let styled = StyledDom::create(&mut dom, css);
1140
1141 let mut cache = GpuValueCache::empty();
1142 let changes = cache.synchronize(&styled);
1143
1144 assert_eq!(
1147 changes.transform_key_changes.len(),
1148 cache.css_transform_keys.len()
1149 );
1150 assert_eq!(changes.opacity_key_changes.len(), cache.opacity_keys.len());
1151 assert_eq!(
1152 cache.css_transform_keys.len(),
1153 cache.css_current_transform_values.len()
1154 );
1155 assert_eq!(cache.opacity_keys.len(), cache.current_opacity_values.len());
1156
1157 let node_count = styled.node_data.len();
1159 assert!(cache.css_transform_keys.keys().all(|n| n.index() < node_count));
1160 assert!(cache.opacity_keys.keys().all(|n| n.index() < node_count));
1161 }
1162}