use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as HashMap;
use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use azul_css::props::style::StyleTransformOrigin;
use crate::{
dom::{DomId, NodeId},
resources::{OpacityKey, TransformKey},
styled_dom::StyledDom,
transform::{ComputedTransform3D, RotationMode, INITIALIZED, USE_AVX, USE_SSE},
};
#[derive(Default, Debug, Clone)]
pub struct GpuValueCache {
pub transform_keys: HashMap<NodeId, TransformKey>,
pub current_transform_values: HashMap<NodeId, ComputedTransform3D>,
pub h_transform_keys: HashMap<NodeId, TransformKey>,
pub h_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
pub css_transform_keys: HashMap<NodeId, TransformKey>,
pub css_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
pub opacity_keys: HashMap<NodeId, OpacityKey>,
pub current_opacity_values: HashMap<NodeId, f32>,
pub scrollbar_v_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
pub scrollbar_h_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
pub scrollbar_v_opacity_values: HashMap<(DomId, NodeId), f32>,
pub scrollbar_h_opacity_values: HashMap<(DomId, NodeId), f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum GpuTransformKeyEvent {
Added(NodeId, TransformKey, ComputedTransform3D),
Changed(
NodeId,
TransformKey,
ComputedTransform3D,
ComputedTransform3D,
),
Removed(NodeId, TransformKey),
}
impl GpuValueCache {
#[must_use] pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn synchronize(&mut self, styled_dom: &StyledDom) -> GpuEventChanges {
Self::init_simd_features();
let transform_key_changes = self.compute_transform_events(styled_dom);
self.apply_transform_events(&transform_key_changes);
let opacity_key_changes = self.compute_opacity_events(styled_dom);
self.apply_opacity_events(&opacity_key_changes);
GpuEventChanges {
transform_key_changes,
opacity_key_changes,
scrollbar_opacity_changes: Vec::new(), }
}
#[allow(clippy::missing_const_for_fn)] fn init_simd_features() {
#[cfg(target_arch = "x86_64")]
unsafe {
if !INITIALIZED.load(AtomicOrdering::SeqCst) {
use core::arch::x86_64::__cpuid;
let mut cpuid = __cpuid(0);
let n_ids = cpuid.eax;
if n_ids > 0 {
cpuid = __cpuid(1);
USE_SSE.store((cpuid.edx & (1_u32 << 25)) != 0, AtomicOrdering::SeqCst);
USE_AVX.store((cpuid.ecx & (1_u32 << 28)) != 0, AtomicOrdering::SeqCst);
}
INITIALIZED.store(true, AtomicOrdering::SeqCst);
}
}
}
fn compute_transform_events(&self, styled_dom: &StyledDom) -> Vec<GpuTransformKeyEvent> {
let css_property_cache = styled_dom.get_css_property_cache();
let node_data = styled_dom.node_data.as_container();
let node_states = styled_dom.styled_nodes.as_container();
let default_transform_origin = StyleTransformOrigin::default();
let mut events = (0..styled_dom.node_data.len())
.filter_map(|node_id| {
let node_id = NodeId::new(node_id);
let styled_node_state = &node_states[node_id].styled_node_state;
if styled_node_state.is_normal() {
if let Some(ref cc) = css_property_cache.compact_cache {
if !cc.has_transform(node_id.index())
&& (self.css_current_transform_values.is_empty()
|| !self.css_current_transform_values.contains_key(&node_id))
{
return None;
}
}
}
let node_data = &node_data[node_id];
let current_transform = css_property_cache
.get_transform(node_data, &node_id, styled_node_state)?
.get_property()
.map(|t| {
let parent_size_width = 0.0;
let parent_size_height = 0.0;
let transform_origin = css_property_cache.get_transform_origin(
node_data,
&node_id,
styled_node_state,
);
let transform_origin = transform_origin
.as_ref()
.and_then(|o| o.get_property())
.unwrap_or(&default_transform_origin);
ComputedTransform3D::from_style_transform_vec(
t.as_ref(),
transform_origin,
parent_size_width,
parent_size_height,
RotationMode::ForWebRender,
)
});
let existing_transform = if self.css_current_transform_values.is_empty() {
None
} else {
self.css_current_transform_values.get(&node_id)
};
match (existing_transform, current_transform) {
(None, None) => None, (None, Some(new)) => Some(GpuTransformKeyEvent::Added(
node_id,
TransformKey::unique(),
new,
)),
(Some(old), Some(new)) => Some(GpuTransformKeyEvent::Changed(
node_id,
self.css_transform_keys.get(&node_id).copied()?,
*old,
new,
)),
(Some(_old), None) => Some(GpuTransformKeyEvent::Removed(
node_id,
self.css_transform_keys.get(&node_id).copied()?,
)),
}
})
.collect::<Vec<GpuTransformKeyEvent>>();
let node_count = styled_dom.node_data.len();
for (node_id, key) in &self.css_transform_keys {
if node_id.index() >= node_count {
events.push(GpuTransformKeyEvent::Removed(*node_id, *key));
}
}
events
}
fn apply_transform_events(&mut self, events: &[GpuTransformKeyEvent]) {
for event in events {
match &event {
GpuTransformKeyEvent::Added(node_id, key, matrix) => {
self.css_transform_keys.insert(*node_id, *key);
self.css_current_transform_values.insert(*node_id, *matrix);
}
GpuTransformKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
self.css_current_transform_values.insert(*node_id, *new_state);
}
GpuTransformKeyEvent::Removed(node_id, _key) => {
self.css_transform_keys.remove(node_id);
self.css_current_transform_values.remove(node_id);
}
}
}
}
fn compute_opacity_events(&self, styled_dom: &StyledDom) -> Vec<GpuOpacityKeyEvent> {
let css_property_cache = styled_dom.get_css_property_cache();
let node_data = styled_dom.node_data.as_container();
let node_states = styled_dom.styled_nodes.as_container();
let mut events = (0..styled_dom.node_data.len())
.filter_map(|node_id| {
let node_id = NodeId::new(node_id);
let styled_node_state = &node_states[node_id].styled_node_state;
let mut compact_opacity: Option<f32> = None;
if styled_node_state.is_normal() {
if let Some(ref cc) = css_property_cache.compact_cache {
let raw = cc.get_opacity_raw(node_id.index());
compact_opacity = if raw == azul_css::compact_cache::OPACITY_SENTINEL {
self.current_opacity_values.get(&node_id)?;
None
} else {
Some(f32::from(raw) / 254.0)
};
}
}
let node_data = &node_data[node_id];
let current_opacity: Option<f32> = if let Some(v) = compact_opacity {
Some(v)
} else if styled_node_state.is_normal() && css_property_cache.compact_cache.is_some() {
None
} else {
css_property_cache
.get_opacity(node_data, &node_id, styled_node_state)?
.get_property()
.map(|p| p.inner.normalized())
};
let existing_opacity = self.current_opacity_values.get(&node_id);
match (existing_opacity, current_opacity) {
(None, None) => None, (None, Some(new)) => Some(GpuOpacityKeyEvent::Added(
node_id,
OpacityKey::unique(),
new,
)),
(Some(old), Some(new)) => Some(GpuOpacityKeyEvent::Changed(
node_id,
self.opacity_keys.get(&node_id).copied()?,
*old,
new,
)),
(Some(_old), None) => Some(GpuOpacityKeyEvent::Removed(
node_id,
self.opacity_keys.get(&node_id).copied()?,
)),
}
})
.collect::<Vec<GpuOpacityKeyEvent>>();
let node_count = styled_dom.node_data.len();
for (node_id, key) in &self.opacity_keys {
if node_id.index() >= node_count {
events.push(GpuOpacityKeyEvent::Removed(*node_id, *key));
}
}
events
}
fn apply_opacity_events(&mut self, events: &[GpuOpacityKeyEvent]) {
for event in events {
match &event {
GpuOpacityKeyEvent::Added(node_id, key, opacity) => {
self.opacity_keys.insert(*node_id, *key);
self.current_opacity_values.insert(*node_id, *opacity);
}
GpuOpacityKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
self.current_opacity_values.insert(*node_id, *new_state);
}
GpuOpacityKeyEvent::Removed(node_id, _key) => {
self.opacity_keys.remove(node_id);
self.current_opacity_values.remove(node_id);
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum GpuScrollbarOpacityEvent {
VerticalAdded(DomId, NodeId, OpacityKey, f32),
VerticalChanged(DomId, NodeId, OpacityKey, f32, f32),
VerticalRemoved(DomId, NodeId, OpacityKey),
HorizontalAdded(DomId, NodeId, OpacityKey, f32),
HorizontalChanged(DomId, NodeId, OpacityKey, f32, f32),
HorizontalRemoved(DomId, NodeId, OpacityKey),
}
#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
pub struct GpuEventChanges {
pub transform_key_changes: Vec<GpuTransformKeyEvent>,
pub opacity_key_changes: Vec<GpuOpacityKeyEvent>,
pub scrollbar_opacity_changes: Vec<GpuScrollbarOpacityEvent>,
}
impl GpuEventChanges {
#[must_use] pub fn empty() -> Self {
Self::default()
}
#[must_use] pub const fn is_empty(&self) -> bool {
self.transform_key_changes.is_empty()
&& self.opacity_key_changes.is_empty()
&& self.scrollbar_opacity_changes.is_empty()
}
pub fn merge(&mut self, other: &mut Self) {
self.transform_key_changes.append(&mut other.transform_key_changes);
self.opacity_key_changes.append(&mut other.opacity_key_changes);
self.scrollbar_opacity_changes.append(&mut other.scrollbar_opacity_changes);
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum GpuOpacityKeyEvent {
Added(NodeId, OpacityKey, f32),
Changed(NodeId, OpacityKey, f32, f32),
Removed(NodeId, OpacityKey),
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod autotest_generated {
use azul_css::css::Css;
use super::*;
use crate::dom::Dom;
fn plain_styled_dom() -> StyledDom {
let mut dom = Dom::create_body();
StyledDom::create(&mut dom, Css::empty())
}
fn styled_dom_from_css(css_src: &str, class: &str) -> StyledDom {
let css = azul_css::parser2::new_from_str(css_src).0;
let mut dom = Dom::create_body()
.with_children(vec![Dom::create_div().with_class(class.to_string().into())].into());
StyledDom::create(&mut dom, css)
}
fn hostile_matrix() -> ComputedTransform3D {
ComputedTransform3D::new(
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
f32::MIN,
f32::MAX,
0.0,
-0.0,
f32::EPSILON,
1.0,
2.0,
3.0,
4.0,
5.0,
6.0,
7.0,
8.0,
)
}
fn scale_matrix(s: f32) -> ComputedTransform3D {
ComputedTransform3D::new(
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,
)
}
#[test]
fn gpu_value_cache_empty_holds_no_keys_and_no_values() {
let cache = GpuValueCache::empty();
assert!(cache.transform_keys.is_empty());
assert!(cache.current_transform_values.is_empty());
assert!(cache.h_transform_keys.is_empty());
assert!(cache.h_current_transform_values.is_empty());
assert!(cache.css_transform_keys.is_empty());
assert!(cache.css_current_transform_values.is_empty());
assert!(cache.opacity_keys.is_empty());
assert!(cache.current_opacity_values.is_empty());
assert!(cache.scrollbar_v_opacity_keys.is_empty());
assert!(cache.scrollbar_h_opacity_keys.is_empty());
assert!(cache.scrollbar_v_opacity_values.is_empty());
assert!(cache.scrollbar_h_opacity_values.is_empty());
}
#[test]
fn gpu_event_changes_empty_equals_default_and_is_empty() {
let changes = GpuEventChanges::empty();
assert_eq!(changes, GpuEventChanges::default());
assert!(changes.is_empty());
assert_eq!(changes.transform_key_changes.len(), 0);
assert_eq!(changes.opacity_key_changes.len(), 0);
assert_eq!(changes.scrollbar_opacity_changes.len(), 0);
}
#[test]
fn is_empty_is_false_when_any_single_event_vec_is_populated() {
let node = NodeId::ZERO;
let mut only_transform = GpuEventChanges::empty();
only_transform
.transform_key_changes
.push(GpuTransformKeyEvent::Removed(node, TransformKey::unique()));
assert!(!only_transform.is_empty());
let mut only_opacity = GpuEventChanges::empty();
only_opacity
.opacity_key_changes
.push(GpuOpacityKeyEvent::Removed(node, OpacityKey::unique()));
assert!(!only_opacity.is_empty());
let mut only_scrollbar = GpuEventChanges::empty();
only_scrollbar
.scrollbar_opacity_changes
.push(GpuScrollbarOpacityEvent::VerticalRemoved(
DomId::ROOT_ID,
node,
OpacityKey::unique(),
));
assert!(!only_scrollbar.is_empty());
}
#[test]
fn merge_moves_every_event_and_drains_the_source() {
let node = NodeId::new(7);
let mut target = GpuEventChanges::empty();
target.transform_key_changes.push(GpuTransformKeyEvent::Added(
node,
TransformKey::unique(),
ComputedTransform3D::IDENTITY,
));
let mut source = GpuEventChanges::empty();
source
.transform_key_changes
.push(GpuTransformKeyEvent::Removed(node, TransformKey::unique()));
source
.opacity_key_changes
.push(GpuOpacityKeyEvent::Added(node, OpacityKey::unique(), 0.25));
source
.scrollbar_opacity_changes
.push(GpuScrollbarOpacityEvent::HorizontalAdded(
DomId::ROOT_ID,
node,
OpacityKey::unique(),
1.0,
));
target.merge(&mut source);
assert!(source.is_empty(), "merge must consume the source");
assert_eq!(target.transform_key_changes.len(), 2);
assert_eq!(target.opacity_key_changes.len(), 1);
assert_eq!(target.scrollbar_opacity_changes.len(), 1);
assert!(matches!(
target.transform_key_changes[0],
GpuTransformKeyEvent::Added(..)
));
assert!(matches!(
target.transform_key_changes[1],
GpuTransformKeyEvent::Removed(..)
));
}
#[test]
fn merge_with_an_empty_set_is_the_identity_in_both_directions() {
let node = NodeId::new(1);
let mut populated = GpuEventChanges::empty();
populated
.opacity_key_changes
.push(GpuOpacityKeyEvent::Added(node, OpacityKey::unique(), 0.5));
let snapshot = populated.clone();
let mut empty = GpuEventChanges::empty();
populated.merge(&mut empty);
assert_eq!(populated, snapshot);
assert!(empty.is_empty());
let mut target = GpuEventChanges::empty();
let mut source = snapshot.clone();
target.merge(&mut source);
assert_eq!(target, snapshot);
assert!(source.is_empty());
}
#[test]
fn merging_large_event_vectors_does_not_panic_and_is_idempotent_when_drained() {
let mut target = GpuEventChanges::empty();
let mut source = GpuEventChanges::empty();
for i in 0..10_000usize {
target
.opacity_key_changes
.push(GpuOpacityKeyEvent::Removed(NodeId::new(i), OpacityKey::unique()));
source
.opacity_key_changes
.push(GpuOpacityKeyEvent::Removed(NodeId::new(i), OpacityKey::unique()));
}
target.merge(&mut source);
assert_eq!(target.opacity_key_changes.len(), 20_000);
assert!(source.is_empty());
target.merge(&mut source);
assert_eq!(target.opacity_key_changes.len(), 20_000);
}
#[test]
fn apply_transform_events_on_an_empty_slice_is_a_noop() {
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[]);
assert!(cache.css_transform_keys.is_empty());
assert!(cache.css_current_transform_values.is_empty());
}
#[test]
fn apply_transform_events_keeps_keys_and_values_in_lockstep() {
let node = NodeId::new(3);
let key = TransformKey::unique();
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[GpuTransformKeyEvent::Added(
node,
key,
ComputedTransform3D::IDENTITY,
)]);
assert_eq!(cache.css_transform_keys.get(&node), Some(&key));
assert_eq!(
cache.css_current_transform_values.get(&node),
Some(&ComputedTransform3D::IDENTITY)
);
let scaled = scale_matrix(2.0);
cache.apply_transform_events(&[GpuTransformKeyEvent::Changed(
node,
key,
ComputedTransform3D::IDENTITY,
scaled,
)]);
assert_eq!(cache.css_transform_keys.get(&node), Some(&key));
assert_eq!(cache.css_current_transform_values.get(&node), Some(&scaled));
cache.apply_transform_events(&[GpuTransformKeyEvent::Removed(node, key)]);
assert!(cache.css_transform_keys.is_empty());
assert!(cache.css_current_transform_values.is_empty());
}
#[test]
fn removing_an_uncached_or_out_of_range_node_does_not_panic() {
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[
GpuTransformKeyEvent::Removed(NodeId::new(usize::MAX), TransformKey::unique()),
GpuTransformKeyEvent::Removed(NodeId::ZERO, TransformKey::unique()),
]);
cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(
NodeId::new(usize::MAX),
OpacityKey::unique(),
)]);
assert!(cache.css_transform_keys.is_empty());
assert!(cache.css_current_transform_values.is_empty());
assert!(cache.opacity_keys.is_empty());
assert!(cache.current_opacity_values.is_empty());
}
#[test]
fn transform_events_are_applied_in_slice_order_within_one_batch() {
let node = NodeId::new(2);
let first = TransformKey::unique();
let second = TransformKey::unique();
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[
GpuTransformKeyEvent::Added(node, first, ComputedTransform3D::IDENTITY),
GpuTransformKeyEvent::Removed(node, first),
]);
assert!(cache.css_transform_keys.is_empty());
assert!(cache.css_current_transform_values.is_empty());
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[
GpuTransformKeyEvent::Removed(node, first),
GpuTransformKeyEvent::Added(node, second, ComputedTransform3D::IDENTITY),
]);
assert_eq!(cache.css_transform_keys.get(&node), Some(&second));
}
#[test]
fn two_added_events_for_one_node_keep_only_the_last_key() {
let node = NodeId::ZERO;
let first = TransformKey::unique();
let second = TransformKey::unique();
assert_ne!(
first, second,
"TransformKey::unique() must never hand out the same id twice"
);
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[
GpuTransformKeyEvent::Added(node, first, ComputedTransform3D::IDENTITY),
GpuTransformKeyEvent::Added(node, second, scale_matrix(3.0)),
]);
assert_eq!(cache.css_transform_keys.len(), 1);
assert_eq!(cache.css_transform_keys.get(&node), Some(&second));
assert_eq!(cache.css_current_transform_values.get(&node), Some(&scale_matrix(3.0)));
}
#[test]
fn a_changed_event_for_an_uncached_node_inserts_a_value_but_no_key() {
let node = NodeId::new(5);
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[GpuTransformKeyEvent::Changed(
node,
TransformKey::unique(),
ComputedTransform3D::IDENTITY,
scale_matrix(2.0),
)]);
assert_eq!(cache.css_current_transform_values.get(&node), Some(&scale_matrix(2.0)));
assert!(
cache.css_transform_keys.is_empty(),
"Changed only writes the value map; the key map stays untouched"
);
}
#[test]
fn non_finite_matrices_are_stored_verbatim_and_never_compare_equal() {
let node = NodeId::new(1);
let mut cache = GpuValueCache::empty();
cache.apply_transform_events(&[GpuTransformKeyEvent::Added(
node,
TransformKey::unique(),
hostile_matrix(),
)]);
let stored = cache
.css_current_transform_values
.get(&node)
.copied()
.expect("the matrix must be cached even when it is full of NaN/Inf");
assert!(stored.m[0][0].is_nan());
assert!(stored.m[0][1].is_infinite() && stored.m[0][1].is_sign_positive());
assert!(stored.m[0][2].is_infinite() && stored.m[0][2].is_sign_negative());
assert_eq!(stored.m[0][3], f32::MIN);
assert_eq!(stored.m[1][0], f32::MAX);
let a = hostile_matrix();
let b = hostile_matrix();
assert_ne!(a, b);
}
#[test]
fn apply_opacity_events_add_change_remove_round_trip() {
let node = NodeId::new(4);
let key = OpacityKey::unique();
let mut cache = GpuValueCache::empty();
cache.apply_opacity_events(&[GpuOpacityKeyEvent::Added(node, key, 0.25)]);
assert_eq!(cache.opacity_keys.get(&node), Some(&key));
assert_eq!(cache.current_opacity_values.get(&node), Some(&0.25));
cache.apply_opacity_events(&[GpuOpacityKeyEvent::Changed(node, key, 0.25, 0.75)]);
assert_eq!(cache.opacity_keys.get(&node), Some(&key));
assert_eq!(cache.current_opacity_values.get(&node), Some(&0.75));
cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(node, key)]);
assert!(cache.opacity_keys.is_empty());
assert!(cache.current_opacity_values.is_empty());
cache.apply_opacity_events(&[GpuOpacityKeyEvent::Removed(node, key)]);
assert!(cache.opacity_keys.is_empty());
}
#[test]
fn apply_opacity_events_stores_out_of_range_values_verbatim() {
let cases = [
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
-1.0,
2.0,
1e30,
-0.0,
];
let mut cache = GpuValueCache::empty();
for (i, value) in cases.iter().enumerate() {
cache.apply_opacity_events(&[GpuOpacityKeyEvent::Added(
NodeId::new(i),
OpacityKey::unique(),
*value,
)]);
}
assert_eq!(cache.current_opacity_values.len(), cases.len());
assert_eq!(cache.opacity_keys.len(), cache.current_opacity_values.len());
assert!(cache.current_opacity_values[&NodeId::new(0)].is_nan());
assert!(cache.current_opacity_values[&NodeId::new(1)].is_infinite());
assert_eq!(cache.current_opacity_values[&NodeId::new(3)], -1.0);
assert_eq!(cache.current_opacity_values[&NodeId::new(4)], 2.0);
assert_eq!(cache.current_opacity_values[&NodeId::new(5)], 1e30);
}
#[test]
fn init_simd_features_is_idempotent() {
GpuValueCache::init_simd_features();
GpuValueCache::init_simd_features();
#[cfg(target_arch = "x86_64")]
{
assert!(
INITIALIZED.load(AtomicOrdering::SeqCst),
"the one-time init flag must be set after the first call"
);
let sse = USE_SSE.load(AtomicOrdering::SeqCst);
let avx = USE_AVX.load(AtomicOrdering::SeqCst);
GpuValueCache::init_simd_features();
assert_eq!(sse, USE_SSE.load(AtomicOrdering::SeqCst));
assert_eq!(avx, USE_AVX.load(AtomicOrdering::SeqCst));
}
}
#[test]
fn synchronize_on_a_transform_and_opacity_free_dom_emits_nothing() {
let styled = plain_styled_dom();
let mut cache = GpuValueCache::empty();
let changes = cache.synchronize(&styled);
assert!(changes.is_empty());
assert!(cache.css_transform_keys.is_empty());
assert!(cache.opacity_keys.is_empty());
}
#[test]
fn synchronize_never_fills_scrollbar_opacity_changes() {
let styled = plain_styled_dom();
let mut cache = GpuValueCache::empty();
cache
.scrollbar_v_opacity_keys
.insert((DomId::ROOT_ID, NodeId::ZERO), OpacityKey::unique());
cache
.scrollbar_v_opacity_values
.insert((DomId::ROOT_ID, NodeId::ZERO), 0.5);
let changes = cache.synchronize(&styled);
assert!(changes.scrollbar_opacity_changes.is_empty());
assert_eq!(cache.scrollbar_v_opacity_keys.len(), 1);
assert_eq!(cache.scrollbar_v_opacity_values.len(), 1);
}
#[test]
fn opacity_round_trips_from_css_through_the_compact_cache_quantizer() {
const STEP: f32 = 1.0 / 254.0;
for (css_value, expected) in [
("0", 0.0f32),
("0.25", 0.25),
("0.5", 0.5),
("1", 1.0),
("50%", 0.5),
("100%", 1.0),
] {
let styled = styled_dom_from_css(&format!(".fade {{ opacity: {css_value}; }}"), "fade");
let mut cache = GpuValueCache::empty();
let changes = cache.synchronize(&styled);
let decoded = changes
.opacity_key_changes
.iter()
.find_map(|e| match e {
GpuOpacityKeyEvent::Added(_, _, v) => Some(*v),
_ => None,
})
.unwrap_or_else(|| panic!("`opacity: {css_value}` produced no Added event"));
assert!(
(decoded - expected).abs() <= STEP,
"`opacity: {css_value}` decoded as {decoded}, expected ~{expected}"
);
assert!(
(0.0..=1.0).contains(&decoded),
"`opacity: {css_value}` decoded to {decoded}, outside [0, 1]"
);
}
}
#[test]
fn re_synchronizing_an_unchanged_dom_never_mints_a_second_key() {
let styled = styled_dom_from_css(".fade { opacity: 0.5; }", "fade");
let mut cache = GpuValueCache::empty();
let first = cache.synchronize(&styled);
let added_first = first
.opacity_key_changes
.iter()
.filter(|e| matches!(e, GpuOpacityKeyEvent::Added(..)))
.count();
assert_eq!(added_first, 1, "the .fade div must get exactly one opacity key");
let keys_after_first = cache.opacity_keys.clone();
let second = cache.synchronize(&styled);
assert!(
!second
.opacity_key_changes
.iter()
.any(|e| matches!(e, GpuOpacityKeyEvent::Added(..))),
"re-syncing an unchanged DOM must not allocate a new OpacityKey (GPU key leak)"
);
assert_eq!(
cache.opacity_keys, keys_after_first,
"the OpacityKey of a node must be stable across syncs"
);
}
#[test]
fn synchronize_evicts_cached_keys_for_nodes_that_no_longer_exist() {
let styled = plain_styled_dom();
assert_eq!(styled.node_data.len(), 1);
let ghost_a = NodeId::new(9_999);
let ghost_b = NodeId::new(usize::MAX); let mut cache = GpuValueCache::empty();
for ghost in [ghost_a, ghost_b] {
cache.css_transform_keys.insert(ghost, TransformKey::unique());
cache
.css_current_transform_values
.insert(ghost, ComputedTransform3D::IDENTITY);
cache.opacity_keys.insert(ghost, OpacityKey::unique());
cache.current_opacity_values.insert(ghost, 0.5);
}
let changes = cache.synchronize(&styled);
assert_eq!(changes.transform_key_changes.len(), 2);
assert_eq!(changes.opacity_key_changes.len(), 2);
assert!(changes
.transform_key_changes
.iter()
.all(|e| matches!(e, GpuTransformKeyEvent::Removed(..))));
assert!(changes
.opacity_key_changes
.iter()
.all(|e| matches!(e, GpuOpacityKeyEvent::Removed(..))));
assert!(cache.css_transform_keys.is_empty());
assert!(cache.css_current_transform_values.is_empty());
assert!(cache.opacity_keys.is_empty());
assert!(cache.current_opacity_values.is_empty());
}
#[test]
fn a_cached_opacity_is_evicted_when_the_node_loses_its_opacity() {
let styled = plain_styled_dom();
let node = NodeId::ZERO;
let key = OpacityKey::unique();
let mut cache = GpuValueCache::empty();
cache.opacity_keys.insert(node, key);
cache.current_opacity_values.insert(node, 0.5);
let changes = cache.synchronize(&styled);
assert_eq!(
changes.opacity_key_changes,
vec![GpuOpacityKeyEvent::Removed(node, key)]
);
assert!(cache.opacity_keys.is_empty());
assert!(cache.current_opacity_values.is_empty());
}
#[test]
fn a_cached_transform_is_evicted_when_the_node_loses_its_transform() {
let styled = plain_styled_dom();
let node = NodeId::ZERO;
let key = TransformKey::unique();
let mut cache = GpuValueCache::empty();
cache.css_transform_keys.insert(node, key);
cache
.css_current_transform_values
.insert(node, ComputedTransform3D::IDENTITY);
let changes = cache.synchronize(&styled);
assert_eq!(
changes.transform_key_changes,
vec![GpuTransformKeyEvent::Removed(node, key)]
);
assert!(
cache.css_transform_keys.is_empty(),
"a stale CSS transform key was not evicted"
);
assert!(
cache.css_current_transform_values.is_empty(),
"a stale CSS transform value was not evicted"
);
}
#[test]
fn synchronize_survives_malformed_and_unicode_css() {
for css_src in [
"",
".fade { opacity: ; }",
".fade { opacity: 🦀; }",
".fade { opacity: -1; }",
".fade { opacity: 99999999999; }",
".fade { opacity: NaN; }",
".fade { transform: }",
".fade { transform: rotate(NaN); }",
".fade { transform: rotate(1e400deg); }",
".fade { transform: rotate(); }",
] {
let styled = styled_dom_from_css(css_src, "fade");
let mut cache = GpuValueCache::empty();
let _ = cache.synchronize(&styled);
assert_eq!(
cache.css_transform_keys.len(),
cache.css_current_transform_values.len(),
"transform key/value maps drifted apart (css: {css_src:?})"
);
assert_eq!(
cache.opacity_keys.len(),
cache.current_opacity_values.len(),
"opacity key/value maps drifted apart (css: {css_src:?})"
);
for value in cache.current_opacity_values.values() {
assert!(
!value.is_nan(),
"a NaN opacity reached the GPU cache (css: {css_src:?})"
);
assert!(
(0.0..=1.0).contains(value),
"an out-of-range opacity ({value}) reached the GPU cache (css: {css_src:?})"
);
}
}
}
#[test]
fn synchronize_on_a_500_node_dom_keeps_every_key_in_range() {
let css = azul_css::parser2::new_from_str(
".t { transform: rotate(45deg); } .o { opacity: 0.25; }",
)
.0;
let children = (0..500usize)
.map(|i| {
let class = if i % 2 == 0 { "t" } else { "o" };
Dom::create_div().with_class(class.to_string().into())
})
.collect::<Vec<Dom>>();
let mut dom = Dom::create_body().with_children(children.into());
let styled = StyledDom::create(&mut dom, css);
let mut cache = GpuValueCache::empty();
let changes = cache.synchronize(&styled);
assert_eq!(
changes.transform_key_changes.len(),
cache.css_transform_keys.len()
);
assert_eq!(changes.opacity_key_changes.len(), cache.opacity_keys.len());
assert_eq!(
cache.css_transform_keys.len(),
cache.css_current_transform_values.len()
);
assert_eq!(cache.opacity_keys.len(), cache.current_opacity_values.len());
let node_count = styled.node_data.len();
assert!(cache.css_transform_keys.keys().all(|n| n.index() < node_count));
assert!(cache.opacity_keys.keys().all(|n| n.index() < node_count));
}
}