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 current_transform = css_property_cache
177 .get_transform(node_data, &node_id, styled_node_state)?
178 .get_property()
179 .map(|t| {
180 let parent_size_width = 0.0;
183 let parent_size_height = 0.0;
184 let transform_origin = css_property_cache.get_transform_origin(
185 node_data,
186 &node_id,
187 styled_node_state,
188 );
189 let transform_origin = transform_origin
190 .as_ref()
191 .and_then(|o| o.get_property())
192 .unwrap_or(&default_transform_origin);
193
194 ComputedTransform3D::from_style_transform_vec(
195 t.as_ref(),
196 transform_origin,
197 parent_size_width,
198 parent_size_height,
199 RotationMode::ForWebRender,
200 )
201 });
202
203 let existing_transform = if self.css_current_transform_values.is_empty() {
204 None
205 } else {
206 self.css_current_transform_values.get(&node_id)
207 };
208
209 match (existing_transform, current_transform) {
210 (None, None) => None, (None, Some(new)) => Some(GpuTransformKeyEvent::Added(
212 node_id,
213 TransformKey::unique(),
214 new,
215 )),
216 (Some(old), Some(new)) => Some(GpuTransformKeyEvent::Changed(
217 node_id,
218 self.css_transform_keys.get(&node_id).copied()?,
219 *old,
220 new,
221 )),
222 (Some(_old), None) => Some(GpuTransformKeyEvent::Removed(
223 node_id,
224 self.css_transform_keys.get(&node_id).copied()?,
225 )),
226 }
227 })
228 .collect::<Vec<GpuTransformKeyEvent>>();
229
230 let node_count = styled_dom.node_data.len();
234 for (node_id, key) in &self.css_transform_keys {
235 if node_id.index() >= node_count {
236 events.push(GpuTransformKeyEvent::Removed(*node_id, *key));
237 }
238 }
239
240 events
241 }
242
243 fn apply_transform_events(&mut self, events: &[GpuTransformKeyEvent]) {
245 for event in events {
247 match &event {
248 GpuTransformKeyEvent::Added(node_id, key, matrix) => {
249 self.css_transform_keys.insert(*node_id, *key);
250 self.css_current_transform_values.insert(*node_id, *matrix);
251 }
252 GpuTransformKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
253 self.css_current_transform_values.insert(*node_id, *new_state);
254 }
255 GpuTransformKeyEvent::Removed(node_id, _key) => {
256 self.css_transform_keys.remove(node_id);
257 self.css_current_transform_values.remove(node_id);
258 }
259 }
260 }
261 }
262
263 fn compute_opacity_events(&self, styled_dom: &StyledDom) -> Vec<GpuOpacityKeyEvent> {
265 let css_property_cache = styled_dom.get_css_property_cache();
266 let node_data = styled_dom.node_data.as_container();
267 let node_states = styled_dom.styled_nodes.as_container();
268
269 let mut events = (0..styled_dom.node_data.len())
276 .filter_map(|node_id| {
277 let node_id = NodeId::new(node_id);
278 let styled_node_state = &node_states[node_id].styled_node_state;
279
280 let mut compact_opacity: Option<f32> = None;
282 if styled_node_state.is_normal() {
283 if let Some(ref cc) = css_property_cache.compact_cache {
284 let raw = cc.get_opacity_raw(node_id.index());
285 compact_opacity = if raw == azul_css::compact_cache::OPACITY_SENTINEL {
286 self.current_opacity_values.get(&node_id)?;
288 None
289 } else {
290 Some(f32::from(raw) / 254.0)
291 };
292 }
293 }
294
295 let node_data = &node_data[node_id];
296 let current_opacity: Option<f32> = if let Some(v) = compact_opacity {
297 Some(v)
299 } else if styled_node_state.is_normal() && css_property_cache.compact_cache.is_some() {
300 None
302 } else {
303 css_property_cache
304 .get_opacity(node_data, &node_id, styled_node_state)?
305 .get_property()
306 .map(|p| p.inner.normalized())
307 };
308 let existing_opacity = self.current_opacity_values.get(&node_id);
309
310 match (existing_opacity, current_opacity) {
311 (None, None) => None, (None, Some(new)) => Some(GpuOpacityKeyEvent::Added(
313 node_id,
314 OpacityKey::unique(),
315 new,
316 )),
317 (Some(old), Some(new)) => Some(GpuOpacityKeyEvent::Changed(
318 node_id,
319 self.opacity_keys.get(&node_id).copied()?,
320 *old,
321 new,
322 )),
323 (Some(_old), None) => Some(GpuOpacityKeyEvent::Removed(
324 node_id,
325 self.opacity_keys.get(&node_id).copied()?,
326 )),
327 }
328 })
329 .collect::<Vec<GpuOpacityKeyEvent>>();
330
331 let node_count = styled_dom.node_data.len();
334 for (node_id, key) in &self.opacity_keys {
335 if node_id.index() >= node_count {
336 events.push(GpuOpacityKeyEvent::Removed(*node_id, *key));
337 }
338 }
339
340 events
341 }
342
343 fn apply_opacity_events(&mut self, events: &[GpuOpacityKeyEvent]) {
345 for event in events {
347 match &event {
348 GpuOpacityKeyEvent::Added(node_id, key, opacity) => {
349 self.opacity_keys.insert(*node_id, *key);
350 self.current_opacity_values.insert(*node_id, *opacity);
351 }
352 GpuOpacityKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
353 self.current_opacity_values.insert(*node_id, *new_state);
354 }
355 GpuOpacityKeyEvent::Removed(node_id, _key) => {
356 self.opacity_keys.remove(node_id);
357 self.current_opacity_values.remove(node_id);
358 }
359 }
360 }
361 }
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
369pub enum GpuScrollbarOpacityEvent {
370 VerticalAdded(DomId, NodeId, OpacityKey, f32),
372 VerticalChanged(DomId, NodeId, OpacityKey, f32, f32),
374 VerticalRemoved(DomId, NodeId, OpacityKey),
376 HorizontalAdded(DomId, NodeId, OpacityKey, f32),
378 HorizontalChanged(DomId, NodeId, OpacityKey, f32, f32),
380 HorizontalRemoved(DomId, NodeId, OpacityKey),
382}
383
384#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
389pub struct GpuEventChanges {
390 pub transform_key_changes: Vec<GpuTransformKeyEvent>,
392 pub opacity_key_changes: Vec<GpuOpacityKeyEvent>,
394 pub scrollbar_opacity_changes: Vec<GpuScrollbarOpacityEvent>,
396}
397
398impl GpuEventChanges {
399 #[must_use] pub fn empty() -> Self {
401 Self::default()
402 }
403
404 #[must_use] pub const fn is_empty(&self) -> bool {
406 self.transform_key_changes.is_empty()
407 && self.opacity_key_changes.is_empty()
408 && self.scrollbar_opacity_changes.is_empty()
409 }
410
411 pub fn merge(&mut self, other: &mut Self) {
415 self.transform_key_changes.append(&mut other.transform_key_changes);
416 self.opacity_key_changes.append(&mut other.opacity_key_changes);
417 self.scrollbar_opacity_changes.append(&mut other.scrollbar_opacity_changes);
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
426pub enum GpuOpacityKeyEvent {
427 Added(NodeId, OpacityKey, f32),
429 Changed(NodeId, OpacityKey, f32, f32),
431 Removed(NodeId, OpacityKey),
433}
434
435#[cfg(test)]
436#[allow(clippy::float_cmp)] mod autotest_generated {
438 use azul_css::css::Css;
439
440 use super::*;
441 use crate::dom::Dom;
442
443 fn plain_styled_dom() -> StyledDom {
446 let mut dom = Dom::create_body();
447 StyledDom::create(&mut dom, Css::empty())
448 }
449
450 fn styled_dom_from_css(css_src: &str, class: &str) -> StyledDom {
452 let css = azul_css::parser2::new_from_str(css_src).0;
453 let mut dom = Dom::create_body()
454 .with_children(vec![Dom::create_div().with_class(class.to_string().into())].into());
455 StyledDom::create(&mut dom, css)
456 }
457
458 fn hostile_matrix() -> ComputedTransform3D {
460 ComputedTransform3D::new(
461 f32::NAN,
462 f32::INFINITY,
463 f32::NEG_INFINITY,
464 f32::MIN,
465 f32::MAX,
466 0.0,
467 -0.0,
468 f32::EPSILON,
469 1.0,
470 2.0,
471 3.0,
472 4.0,
473 5.0,
474 6.0,
475 7.0,
476 8.0,
477 )
478 }
479
480 fn scale_matrix(s: f32) -> ComputedTransform3D {
481 ComputedTransform3D::new(
482 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,
483 )
484 }
485
486 #[test]
491 fn gpu_value_cache_empty_holds_no_keys_and_no_values() {
492 let cache = GpuValueCache::empty();
493 assert!(cache.transform_keys.is_empty());
494 assert!(cache.current_transform_values.is_empty());
495 assert!(cache.h_transform_keys.is_empty());
496 assert!(cache.h_current_transform_values.is_empty());
497 assert!(cache.css_transform_keys.is_empty());
498 assert!(cache.css_current_transform_values.is_empty());
499 assert!(cache.opacity_keys.is_empty());
500 assert!(cache.current_opacity_values.is_empty());
501 assert!(cache.scrollbar_v_opacity_keys.is_empty());
502 assert!(cache.scrollbar_h_opacity_keys.is_empty());
503 assert!(cache.scrollbar_v_opacity_values.is_empty());
504 assert!(cache.scrollbar_h_opacity_values.is_empty());
505 }
506
507 #[test]
508 fn gpu_event_changes_empty_equals_default_and_is_empty() {
509 let changes = GpuEventChanges::empty();
510 assert_eq!(changes, GpuEventChanges::default());
511 assert!(changes.is_empty());
512 assert_eq!(changes.transform_key_changes.len(), 0);
513 assert_eq!(changes.opacity_key_changes.len(), 0);
514 assert_eq!(changes.scrollbar_opacity_changes.len(), 0);
515 }
516
517 #[test]
522 fn is_empty_is_false_when_any_single_event_vec_is_populated() {
523 let node = NodeId::ZERO;
524
525 let mut only_transform = GpuEventChanges::empty();
526 only_transform
527 .transform_key_changes
528 .push(GpuTransformKeyEvent::Removed(node, TransformKey::unique()));
529 assert!(!only_transform.is_empty());
530
531 let mut only_opacity = GpuEventChanges::empty();
532 only_opacity
533 .opacity_key_changes
534 .push(GpuOpacityKeyEvent::Removed(node, OpacityKey::unique()));
535 assert!(!only_opacity.is_empty());
536
537 let mut only_scrollbar = GpuEventChanges::empty();
540 only_scrollbar
541 .scrollbar_opacity_changes
542 .push(GpuScrollbarOpacityEvent::VerticalRemoved(
543 DomId::ROOT_ID,
544 node,
545 OpacityKey::unique(),
546 ));
547 assert!(!only_scrollbar.is_empty());
548 }
549
550 #[test]
555 fn merge_moves_every_event_and_drains_the_source() {
556 let node = NodeId::new(7);
557
558 let mut target = GpuEventChanges::empty();
559 target.transform_key_changes.push(GpuTransformKeyEvent::Added(
560 node,
561 TransformKey::unique(),
562 ComputedTransform3D::IDENTITY,
563 ));
564
565 let mut source = GpuEventChanges::empty();
566 source
567 .transform_key_changes
568 .push(GpuTransformKeyEvent::Removed(node, TransformKey::unique()));
569 source
570 .opacity_key_changes
571 .push(GpuOpacityKeyEvent::Added(node, OpacityKey::unique(), 0.25));
572 source
573 .scrollbar_opacity_changes
574 .push(GpuScrollbarOpacityEvent::HorizontalAdded(
575 DomId::ROOT_ID,
576 node,
577 OpacityKey::unique(),
578 1.0,
579 ));
580
581 target.merge(&mut source);
582
583 assert!(source.is_empty(), "merge must consume the source");
584 assert_eq!(target.transform_key_changes.len(), 2);
585 assert_eq!(target.opacity_key_changes.len(), 1);
586 assert_eq!(target.scrollbar_opacity_changes.len(), 1);
587 assert!(matches!(
589 target.transform_key_changes[0],
590 GpuTransformKeyEvent::Added(..)
591 ));
592 assert!(matches!(
593 target.transform_key_changes[1],
594 GpuTransformKeyEvent::Removed(..)
595 ));
596 }
597
598 #[test]
599 fn merge_with_an_empty_set_is_the_identity_in_both_directions() {
600 let node = NodeId::new(1);
601 let mut populated = GpuEventChanges::empty();
602 populated
603 .opacity_key_changes
604 .push(GpuOpacityKeyEvent::Added(node, OpacityKey::unique(), 0.5));
605 let snapshot = populated.clone();
606
607 let mut empty = GpuEventChanges::empty();
609 populated.merge(&mut empty);
610 assert_eq!(populated, snapshot);
611 assert!(empty.is_empty());
612
613 let mut target = GpuEventChanges::empty();
615 let mut source = snapshot.clone();
616 target.merge(&mut source);
617 assert_eq!(target, snapshot);
618 assert!(source.is_empty());
619 }
620
621 #[test]
622 fn merging_large_event_vectors_does_not_panic_and_is_idempotent_when_drained() {
623 let mut target = GpuEventChanges::empty();
624 let mut source = GpuEventChanges::empty();
625 for i in 0..10_000usize {
626 target
627 .opacity_key_changes
628 .push(GpuOpacityKeyEvent::Removed(NodeId::new(i), OpacityKey::unique()));
629 source
630 .opacity_key_changes
631 .push(GpuOpacityKeyEvent::Removed(NodeId::new(i), OpacityKey::unique()));
632 }
633
634 target.merge(&mut source);
635 assert_eq!(target.opacity_key_changes.len(), 20_000);
636 assert!(source.is_empty());
637
638 target.merge(&mut source);
640 assert_eq!(target.opacity_key_changes.len(), 20_000);
641 }
642
643 #[test]
648 fn apply_transform_events_on_an_empty_slice_is_a_noop() {
649 let mut cache = GpuValueCache::empty();
650 cache.apply_transform_events(&[]);
651 assert!(cache.css_transform_keys.is_empty());
652 assert!(cache.css_current_transform_values.is_empty());
653 }
654
655 #[test]
656 fn apply_transform_events_keeps_keys_and_values_in_lockstep() {
657 let node = NodeId::new(3);
658 let key = TransformKey::unique();
659 let mut cache = GpuValueCache::empty();
660
661 cache.apply_transform_events(&[GpuTransformKeyEvent::Added(
662 node,
663 key,
664 ComputedTransform3D::IDENTITY,
665 )]);
666 assert_eq!(cache.css_transform_keys.get(&node), Some(&key));
667 assert_eq!(
668 cache.css_current_transform_values.get(&node),
669 Some(&ComputedTransform3D::IDENTITY)
670 );
671
672 let scaled = scale_matrix(2.0);
675 cache.apply_transform_events(&[GpuTransformKeyEvent::Changed(
676 node,
677 key,
678 ComputedTransform3D::IDENTITY,
679 scaled,
680 )]);
681 assert_eq!(cache.css_transform_keys.get(&node), Some(&key));
682 assert_eq!(cache.css_current_transform_values.get(&node), Some(&scaled));
683
684 cache.apply_transform_events(&[GpuTransformKeyEvent::Removed(node, key)]);
685 assert!(cache.css_transform_keys.is_empty());
686 assert!(cache.css_current_transform_values.is_empty());
687 }
688
689 #[test]
690 fn removing_an_uncached_or_out_of_range_node_does_not_panic() {
691 let mut cache = GpuValueCache::empty();
692 cache.apply_transform_events(&[
695 GpuTransformKeyEvent::Removed(NodeId::new(usize::MAX), TransformKey::unique()),
696 GpuTransformKeyEvent::Removed(NodeId::ZERO, TransformKey::unique()),
697 ]);
698 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(
699 NodeId::new(usize::MAX),
700 OpacityKey::unique(),
701 )]);
702 assert!(cache.css_transform_keys.is_empty());
703 assert!(cache.css_current_transform_values.is_empty());
704 assert!(cache.opacity_keys.is_empty());
705 assert!(cache.current_opacity_values.is_empty());
706 }
707
708 #[test]
709 fn transform_events_are_applied_in_slice_order_within_one_batch() {
710 let node = NodeId::new(2);
711 let first = TransformKey::unique();
712 let second = TransformKey::unique();
713
714 let mut cache = GpuValueCache::empty();
716 cache.apply_transform_events(&[
717 GpuTransformKeyEvent::Added(node, first, ComputedTransform3D::IDENTITY),
718 GpuTransformKeyEvent::Removed(node, first),
719 ]);
720 assert!(cache.css_transform_keys.is_empty());
721 assert!(cache.css_current_transform_values.is_empty());
722
723 let mut cache = GpuValueCache::empty();
725 cache.apply_transform_events(&[
726 GpuTransformKeyEvent::Removed(node, first),
727 GpuTransformKeyEvent::Added(node, second, ComputedTransform3D::IDENTITY),
728 ]);
729 assert_eq!(cache.css_transform_keys.get(&node), Some(&second));
730 }
731
732 #[test]
733 fn two_added_events_for_one_node_keep_only_the_last_key() {
734 let node = NodeId::ZERO;
735 let first = TransformKey::unique();
736 let second = TransformKey::unique();
737 assert_ne!(
738 first, second,
739 "TransformKey::unique() must never hand out the same id twice"
740 );
741
742 let mut cache = GpuValueCache::empty();
743 cache.apply_transform_events(&[
744 GpuTransformKeyEvent::Added(node, first, ComputedTransform3D::IDENTITY),
745 GpuTransformKeyEvent::Added(node, second, scale_matrix(3.0)),
746 ]);
747 assert_eq!(cache.css_transform_keys.len(), 1);
748 assert_eq!(cache.css_transform_keys.get(&node), Some(&second));
749 assert_eq!(cache.css_current_transform_values.get(&node), Some(&scale_matrix(3.0)));
750 }
751
752 #[test]
753 fn a_changed_event_for_an_uncached_node_inserts_a_value_but_no_key() {
754 let node = NodeId::new(5);
757 let mut cache = GpuValueCache::empty();
758 cache.apply_transform_events(&[GpuTransformKeyEvent::Changed(
759 node,
760 TransformKey::unique(),
761 ComputedTransform3D::IDENTITY,
762 scale_matrix(2.0),
763 )]);
764 assert_eq!(cache.css_current_transform_values.get(&node), Some(&scale_matrix(2.0)));
765 assert!(
766 cache.css_transform_keys.is_empty(),
767 "Changed only writes the value map; the key map stays untouched"
768 );
769 }
770
771 #[test]
772 fn non_finite_matrices_are_stored_verbatim_and_never_compare_equal() {
773 let node = NodeId::new(1);
774 let mut cache = GpuValueCache::empty();
775 cache.apply_transform_events(&[GpuTransformKeyEvent::Added(
776 node,
777 TransformKey::unique(),
778 hostile_matrix(),
779 )]);
780
781 let stored = cache
782 .css_current_transform_values
783 .get(&node)
784 .copied()
785 .expect("the matrix must be cached even when it is full of NaN/Inf");
786 assert!(stored.m[0][0].is_nan());
787 assert!(stored.m[0][1].is_infinite() && stored.m[0][1].is_sign_positive());
788 assert!(stored.m[0][2].is_infinite() && stored.m[0][2].is_sign_negative());
789 assert_eq!(stored.m[0][3], f32::MIN);
790 assert_eq!(stored.m[1][0], f32::MAX);
791
792 let a = hostile_matrix();
795 let b = hostile_matrix();
796 assert_ne!(a, b);
797 }
798
799 #[test]
804 fn apply_opacity_events_add_change_remove_round_trip() {
805 let node = NodeId::new(4);
806 let key = OpacityKey::unique();
807 let mut cache = GpuValueCache::empty();
808
809 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Added(node, key, 0.25)]);
810 assert_eq!(cache.opacity_keys.get(&node), Some(&key));
811 assert_eq!(cache.current_opacity_values.get(&node), Some(&0.25));
812
813 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Changed(node, key, 0.25, 0.75)]);
814 assert_eq!(cache.opacity_keys.get(&node), Some(&key));
815 assert_eq!(cache.current_opacity_values.get(&node), Some(&0.75));
816
817 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(node, key)]);
818 assert!(cache.opacity_keys.is_empty());
819 assert!(cache.current_opacity_values.is_empty());
820
821 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(node, key)]);
823 assert!(cache.opacity_keys.is_empty());
824 }
825
826 #[test]
827 fn apply_opacity_events_stores_out_of_range_values_verbatim() {
828 let cases = [
831 f32::NAN,
832 f32::INFINITY,
833 f32::NEG_INFINITY,
834 -1.0,
835 2.0,
836 1e30,
837 -0.0,
838 ];
839 let mut cache = GpuValueCache::empty();
840 for (i, value) in cases.iter().enumerate() {
841 cache.apply_opacity_events(&[GpuOpacityKeyEvent::Added(
842 NodeId::new(i),
843 OpacityKey::unique(),
844 *value,
845 )]);
846 }
847
848 assert_eq!(cache.current_opacity_values.len(), cases.len());
849 assert_eq!(cache.opacity_keys.len(), cache.current_opacity_values.len());
850 assert!(cache.current_opacity_values[&NodeId::new(0)].is_nan());
851 assert!(cache.current_opacity_values[&NodeId::new(1)].is_infinite());
852 assert_eq!(cache.current_opacity_values[&NodeId::new(3)], -1.0);
853 assert_eq!(cache.current_opacity_values[&NodeId::new(4)], 2.0);
854 assert_eq!(cache.current_opacity_values[&NodeId::new(5)], 1e30);
855 }
856
857 #[test]
862 fn init_simd_features_is_idempotent() {
863 GpuValueCache::init_simd_features();
864 GpuValueCache::init_simd_features();
865
866 #[cfg(target_arch = "x86_64")]
867 {
868 assert!(
869 INITIALIZED.load(AtomicOrdering::SeqCst),
870 "the one-time init flag must be set after the first call"
871 );
872 let sse = USE_SSE.load(AtomicOrdering::SeqCst);
873 let avx = USE_AVX.load(AtomicOrdering::SeqCst);
874 GpuValueCache::init_simd_features();
875 assert_eq!(sse, USE_SSE.load(AtomicOrdering::SeqCst));
876 assert_eq!(avx, USE_AVX.load(AtomicOrdering::SeqCst));
877 }
878 }
879
880 #[test]
885 fn synchronize_on_a_transform_and_opacity_free_dom_emits_nothing() {
886 let styled = plain_styled_dom();
887 let mut cache = GpuValueCache::empty();
888
889 let changes = cache.synchronize(&styled);
890
891 assert!(changes.is_empty());
892 assert!(cache.css_transform_keys.is_empty());
893 assert!(cache.opacity_keys.is_empty());
894 }
895
896 #[test]
897 fn synchronize_never_fills_scrollbar_opacity_changes() {
898 let styled = plain_styled_dom();
901 let mut cache = GpuValueCache::empty();
902 cache
903 .scrollbar_v_opacity_keys
904 .insert((DomId::ROOT_ID, NodeId::ZERO), OpacityKey::unique());
905 cache
906 .scrollbar_v_opacity_values
907 .insert((DomId::ROOT_ID, NodeId::ZERO), 0.5);
908
909 let changes = cache.synchronize(&styled);
910
911 assert!(changes.scrollbar_opacity_changes.is_empty());
912 assert_eq!(cache.scrollbar_v_opacity_keys.len(), 1);
913 assert_eq!(cache.scrollbar_v_opacity_values.len(), 1);
914 }
915
916 #[test]
917 fn opacity_round_trips_from_css_through_the_compact_cache_quantizer() {
918 const STEP: f32 = 1.0 / 254.0;
922 for (css_value, expected) in [
923 ("0", 0.0f32),
924 ("0.25", 0.25),
925 ("0.5", 0.5),
926 ("1", 1.0),
927 ("50%", 0.5),
928 ("100%", 1.0),
929 ] {
930 let styled = styled_dom_from_css(&format!(".fade {{ opacity: {css_value}; }}"), "fade");
931 let mut cache = GpuValueCache::empty();
932
933 let changes = cache.synchronize(&styled);
934
935 let decoded = changes
936 .opacity_key_changes
937 .iter()
938 .find_map(|e| match e {
939 GpuOpacityKeyEvent::Added(_, _, v) => Some(*v),
940 _ => None,
941 })
942 .unwrap_or_else(|| panic!("`opacity: {css_value}` produced no Added event"));
943
944 assert!(
945 (decoded - expected).abs() <= STEP,
946 "`opacity: {css_value}` decoded as {decoded}, expected ~{expected}"
947 );
948 assert!(
949 (0.0..=1.0).contains(&decoded),
950 "`opacity: {css_value}` decoded to {decoded}, outside [0, 1]"
951 );
952 }
953 }
954
955 #[test]
956 fn re_synchronizing_an_unchanged_dom_never_mints_a_second_key() {
957 let styled = styled_dom_from_css(".fade { opacity: 0.5; }", "fade");
958 let mut cache = GpuValueCache::empty();
959
960 let first = cache.synchronize(&styled);
961 let added_first = first
962 .opacity_key_changes
963 .iter()
964 .filter(|e| matches!(e, GpuOpacityKeyEvent::Added(..)))
965 .count();
966 assert_eq!(added_first, 1, "the .fade div must get exactly one opacity key");
967 let keys_after_first = cache.opacity_keys.clone();
968
969 let second = cache.synchronize(&styled);
970
971 assert!(
972 !second
973 .opacity_key_changes
974 .iter()
975 .any(|e| matches!(e, GpuOpacityKeyEvent::Added(..))),
976 "re-syncing an unchanged DOM must not allocate a new OpacityKey (GPU key leak)"
977 );
978 assert_eq!(
979 cache.opacity_keys, keys_after_first,
980 "the OpacityKey of a node must be stable across syncs"
981 );
982 }
983
984 #[test]
985 fn synchronize_evicts_cached_keys_for_nodes_that_no_longer_exist() {
986 let styled = plain_styled_dom();
989 assert_eq!(styled.node_data.len(), 1);
990
991 let ghost_a = NodeId::new(9_999);
992 let ghost_b = NodeId::new(usize::MAX); let mut cache = GpuValueCache::empty();
994 for ghost in [ghost_a, ghost_b] {
995 cache.css_transform_keys.insert(ghost, TransformKey::unique());
996 cache
997 .css_current_transform_values
998 .insert(ghost, ComputedTransform3D::IDENTITY);
999 cache.opacity_keys.insert(ghost, OpacityKey::unique());
1000 cache.current_opacity_values.insert(ghost, 0.5);
1001 }
1002
1003 let changes = cache.synchronize(&styled);
1004
1005 assert_eq!(changes.transform_key_changes.len(), 2);
1006 assert_eq!(changes.opacity_key_changes.len(), 2);
1007 assert!(changes
1008 .transform_key_changes
1009 .iter()
1010 .all(|e| matches!(e, GpuTransformKeyEvent::Removed(..))));
1011 assert!(changes
1012 .opacity_key_changes
1013 .iter()
1014 .all(|e| matches!(e, GpuOpacityKeyEvent::Removed(..))));
1015 assert!(cache.css_transform_keys.is_empty());
1016 assert!(cache.css_current_transform_values.is_empty());
1017 assert!(cache.opacity_keys.is_empty());
1018 assert!(cache.current_opacity_values.is_empty());
1019 }
1020
1021 #[test]
1022 fn a_cached_opacity_is_evicted_when_the_node_loses_its_opacity() {
1023 let styled = plain_styled_dom();
1026 let node = NodeId::ZERO;
1027 let key = OpacityKey::unique();
1028
1029 let mut cache = GpuValueCache::empty();
1030 cache.opacity_keys.insert(node, key);
1031 cache.current_opacity_values.insert(node, 0.5);
1032
1033 let changes = cache.synchronize(&styled);
1034
1035 assert_eq!(
1036 changes.opacity_key_changes,
1037 vec![GpuOpacityKeyEvent::Removed(node, key)]
1038 );
1039 assert!(cache.opacity_keys.is_empty());
1040 assert!(cache.current_opacity_values.is_empty());
1041 }
1042
1043 #[test]
1044 fn a_cached_transform_is_evicted_when_the_node_loses_its_transform() {
1045 let styled = plain_styled_dom();
1051 let node = NodeId::ZERO;
1052 let key = TransformKey::unique();
1053
1054 let mut cache = GpuValueCache::empty();
1055 cache.css_transform_keys.insert(node, key);
1056 cache
1057 .css_current_transform_values
1058 .insert(node, ComputedTransform3D::IDENTITY);
1059
1060 let changes = cache.synchronize(&styled);
1061
1062 assert_eq!(
1063 changes.transform_key_changes,
1064 vec![GpuTransformKeyEvent::Removed(node, key)]
1065 );
1066 assert!(
1067 cache.css_transform_keys.is_empty(),
1068 "a stale CSS transform key was not evicted"
1069 );
1070 assert!(
1071 cache.css_current_transform_values.is_empty(),
1072 "a stale CSS transform value was not evicted"
1073 );
1074 }
1075
1076 #[test]
1077 fn synchronize_survives_malformed_and_unicode_css() {
1078 for css_src in [
1079 "",
1080 ".fade { opacity: ; }",
1081 ".fade { opacity: 🦀; }",
1082 ".fade { opacity: -1; }",
1083 ".fade { opacity: 99999999999; }",
1084 ".fade { opacity: NaN; }",
1085 ".fade { transform: }",
1086 ".fade { transform: rotate(NaN); }",
1087 ".fade { transform: rotate(1e400deg); }",
1088 ".fade { transform: rotate(); }",
1089 ] {
1090 let styled = styled_dom_from_css(css_src, "fade");
1091 let mut cache = GpuValueCache::empty();
1092
1093 let _ = cache.synchronize(&styled);
1094
1095 assert_eq!(
1097 cache.css_transform_keys.len(),
1098 cache.css_current_transform_values.len(),
1099 "transform key/value maps drifted apart (css: {css_src:?})"
1100 );
1101 assert_eq!(
1102 cache.opacity_keys.len(),
1103 cache.current_opacity_values.len(),
1104 "opacity key/value maps drifted apart (css: {css_src:?})"
1105 );
1106 for value in cache.current_opacity_values.values() {
1107 assert!(
1108 !value.is_nan(),
1109 "a NaN opacity reached the GPU cache (css: {css_src:?})"
1110 );
1111 assert!(
1112 (0.0..=1.0).contains(value),
1113 "an out-of-range opacity ({value}) reached the GPU cache (css: {css_src:?})"
1114 );
1115 }
1116 }
1117 }
1118
1119 #[test]
1120 fn synchronize_on_a_500_node_dom_keeps_every_key_in_range() {
1121 let css = azul_css::parser2::new_from_str(
1122 ".t { transform: rotate(45deg); } .o { opacity: 0.25; }",
1123 )
1124 .0;
1125 let children = (0..500usize)
1126 .map(|i| {
1127 let class = if i % 2 == 0 { "t" } else { "o" };
1128 Dom::create_div().with_class(class.to_string().into())
1129 })
1130 .collect::<Vec<Dom>>();
1131 let mut dom = Dom::create_body().with_children(children.into());
1132 let styled = StyledDom::create(&mut dom, css);
1133
1134 let mut cache = GpuValueCache::empty();
1135 let changes = cache.synchronize(&styled);
1136
1137 assert_eq!(
1140 changes.transform_key_changes.len(),
1141 cache.css_transform_keys.len()
1142 );
1143 assert_eq!(changes.opacity_key_changes.len(), cache.opacity_keys.len());
1144 assert_eq!(
1145 cache.css_transform_keys.len(),
1146 cache.css_current_transform_values.len()
1147 );
1148 assert_eq!(cache.opacity_keys.len(), cache.current_opacity_values.len());
1149
1150 let node_count = styled.node_data.len();
1152 assert!(cache.css_transform_keys.keys().all(|n| n.index() < node_count));
1153 assert!(cache.opacity_keys.keys().all(|n| n.index() < node_count));
1154 }
1155}