Skip to main content

azul_core/
gpu.rs

1//! GPU value caching for CSS transforms and opacity.
2//!
3//! This module manages the synchronization between DOM CSS properties (transforms and opacity)
4//! and GPU-side keys used by WebRender. It tracks changes to transform and opacity values
5//! and generates events when values are added, changed, or removed.
6//!
7//! # Performance
8//!
9//! The cache uses CPU feature detection (SSE/AVX on x86_64) to optimize transform calculations.
10//! Values are only recalculated when CSS properties change, minimizing GPU updates.
11//!
12//! # Architecture
13//!
14//! - `GpuValueCache`: Stores current transform/opacity keys and values for all nodes
15//! - `GpuEventChanges`: Contains delta events for transform/opacity changes
16//! - `GpuTransformKeyEvent`: Events for transform additions, changes, and removals
17//!
18//! The cache is synchronized with the `StyledDom` on each frame, generating minimal
19//! update events to send to the GPU.
20
21use alloc::{collections::BTreeMap, vec::Vec};
22use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
23
24use azul_css::props::{basic::LayoutSize, style::StyleTransformOrigin};
25
26use crate::{
27    dom::{DomId, NodeId},
28    id::NodeDataContainerRef,
29    resources::{OpacityKey, TransformKey},
30    styled_dom::StyledDom,
31    transform::{ComputedTransform3D, RotationMode, INITIALIZED, USE_AVX, USE_SSE},
32};
33
34/// Caches GPU transform and opacity keys and their current values for all nodes.
35///
36/// This cache stores the WebRender keys and computed values for nodes with
37/// CSS transforms or opacity. It's synchronized with the `StyledDom` to detect
38/// changes and generate minimal update events.
39///
40/// # Fields
41///
42/// * `transform_keys` - Maps node IDs to their WebRender transform keys
43/// * `current_transform_values` - Current computed transform for each node
44/// * `opacity_keys` - Maps node IDs to their WebRender opacity keys
45/// * `current_opacity_values` - Current opacity value for each node
46/// * `scrollbar_v_opacity_keys` - Maps (DomId, NodeId) to vertical scrollbar opacity keys
47/// * `scrollbar_h_opacity_keys` - Maps (DomId, NodeId) to horizontal scrollbar opacity keys
48/// * `scrollbar_v_opacity_values` - Current vertical scrollbar opacity values
49/// * `scrollbar_h_opacity_values` - Current horizontal scrollbar opacity values
50#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
51pub struct GpuValueCache {
52    pub transform_keys: BTreeMap<NodeId, TransformKey>,
53    pub current_transform_values: BTreeMap<NodeId, ComputedTransform3D>,
54    pub opacity_keys: BTreeMap<NodeId, OpacityKey>,
55    pub current_opacity_values: BTreeMap<NodeId, f32>,
56    pub scrollbar_v_opacity_keys: BTreeMap<(DomId, NodeId), OpacityKey>,
57    pub scrollbar_h_opacity_keys: BTreeMap<(DomId, NodeId), OpacityKey>,
58    pub scrollbar_v_opacity_values: BTreeMap<(DomId, NodeId), f32>,
59    pub scrollbar_h_opacity_values: BTreeMap<(DomId, NodeId), f32>,
60}
61
62/// Represents a change to a GPU transform key.
63///
64/// These events are generated when synchronizing the cache with the `StyledDom`
65/// and are used to update WebRender's transform state efficiently.
66#[derive(Debug, Clone, PartialEq, PartialOrd)]
67pub enum GpuTransformKeyEvent {
68    /// A new transform was added to a node
69    Added(NodeId, TransformKey, ComputedTransform3D),
70    /// An existing transform was modified (includes old and new values)
71    Changed(
72        NodeId,
73        TransformKey,
74        ComputedTransform3D,
75        ComputedTransform3D,
76    ),
77    /// A transform was removed from a node
78    Removed(NodeId, TransformKey),
79}
80
81impl GpuValueCache {
82    /// Creates an empty GPU value cache.
83    pub fn empty() -> Self {
84        Self::default()
85    }
86
87    /// Synchronizes the cache with the current `StyledDom`, generating change events.
88    ///
89    /// This method:
90    /// 1. Computes current transform and opacity values from CSS properties
91    /// 2. Compares with cached values to detect changes
92    /// 3. Generates events for additions, changes, and removals
93    /// 4. Updates the internal cache state
94    ///
95    /// # Performance
96    ///
97    /// On x86_64, this function detects and uses SSE/AVX instructions for
98    /// optimized transform calculations.
99    ///
100    /// # Returns
101    ///
102    /// `GpuEventChanges` containing all transform and opacity change events.
103    #[must_use]
104    pub fn synchronize<'a>(&mut self, styled_dom: &StyledDom) -> GpuEventChanges {
105        let css_property_cache = styled_dom.get_css_property_cache();
106        let node_data = styled_dom.node_data.as_container();
107        let node_states = styled_dom.styled_nodes.as_container();
108
109        let default_transform_origin = StyleTransformOrigin::default();
110
111        #[cfg(target_arch = "x86_64")]
112        unsafe {
113            if !INITIALIZED.load(AtomicOrdering::SeqCst) {
114                use core::arch::x86_64::__cpuid;
115
116                let mut cpuid = __cpuid(0);
117                let n_ids = cpuid.eax;
118
119                if n_ids > 0 {
120                    // cpuid instruction is present
121                    cpuid = __cpuid(1);
122                    USE_SSE.store((cpuid.edx & (1_u32 << 25)) != 0, AtomicOrdering::SeqCst);
123                    USE_AVX.store((cpuid.ecx & (1_u32 << 28)) != 0, AtomicOrdering::SeqCst);
124                }
125                INITIALIZED.store(true, AtomicOrdering::SeqCst);
126            }
127        }
128
129        // calculate the transform values of every single node that has a non-default transform
130        let all_current_transform_events = (0..styled_dom.node_data.len())
131            .into_iter()
132            .filter_map(|node_id| {
133                let node_id = NodeId::new(node_id);
134                let styled_node_state = &node_states[node_id].styled_node_state;
135                let node_data = &node_data[node_id];
136                let current_transform = css_property_cache
137                    .get_transform(node_data, &node_id, styled_node_state)?
138                    .get_property()
139                    .map(|t| {
140                        // TODO: look up the parent nodes size properly to resolve animation of
141                        // transforms with %
142                        let parent_size_width = 0.0;
143                        let parent_size_height = 0.0;
144                        let transform_origin = css_property_cache.get_transform_origin(
145                            node_data,
146                            &node_id,
147                            styled_node_state,
148                        );
149                        let transform_origin = transform_origin
150                            .as_ref()
151                            .and_then(|o| o.get_property())
152                            .unwrap_or(&default_transform_origin);
153
154                        ComputedTransform3D::from_style_transform_vec(
155                            t.as_ref(),
156                            transform_origin,
157                            parent_size_width,
158                            parent_size_height,
159                            RotationMode::ForWebRender,
160                        )
161                    });
162
163                let existing_transform = self.current_transform_values.get(&node_id);
164
165                match (existing_transform, current_transform) {
166                    (None, None) => None, // no new transform, no old transform
167                    (None, Some(new)) => Some(GpuTransformKeyEvent::Added(
168                        node_id,
169                        TransformKey::unique(),
170                        new,
171                    )),
172                    (Some(old), Some(new)) => Some(GpuTransformKeyEvent::Changed(
173                        node_id,
174                        self.transform_keys.get(&node_id).copied()?,
175                        *old,
176                        new,
177                    )),
178                    (Some(_old), None) => Some(GpuTransformKeyEvent::Removed(
179                        node_id,
180                        self.transform_keys.get(&node_id).copied()?,
181                    )),
182                }
183            })
184            .collect::<Vec<GpuTransformKeyEvent>>();
185
186        // remove / add the transform keys accordingly
187        for event in all_current_transform_events.iter() {
188            match &event {
189                GpuTransformKeyEvent::Added(node_id, key, matrix) => {
190                    self.transform_keys.insert(*node_id, *key);
191                    self.current_transform_values.insert(*node_id, *matrix);
192                }
193                GpuTransformKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
194                    self.current_transform_values.insert(*node_id, *new_state);
195                }
196                GpuTransformKeyEvent::Removed(node_id, _key) => {
197                    self.transform_keys.remove(node_id);
198                    self.current_transform_values.remove(node_id);
199                }
200            }
201        }
202
203        // calculate the opacity of every single node that has a non-default opacity
204        let all_current_opacity_events = (0..styled_dom.node_data.len())
205            .into_iter()
206            .filter_map(|node_id| {
207                let node_id = NodeId::new(node_id);
208                let styled_node_state = &node_states[node_id].styled_node_state;
209                let node_data = &node_data[node_id];
210                let current_opacity =
211                    css_property_cache.get_opacity(node_data, &node_id, styled_node_state)?;
212                let current_opacity = current_opacity.get_property();
213                let existing_opacity = self.current_opacity_values.get(&node_id);
214
215                match (existing_opacity, current_opacity) {
216                    (None, None) => None, // no new opacity, no old transform
217                    (None, Some(new)) => Some(GpuOpacityKeyEvent::Added(
218                        node_id,
219                        OpacityKey::unique(),
220                        new.inner.normalized(),
221                    )),
222                    (Some(old), Some(new)) => Some(GpuOpacityKeyEvent::Changed(
223                        node_id,
224                        self.opacity_keys.get(&node_id).copied()?,
225                        *old,
226                        new.inner.normalized(),
227                    )),
228                    (Some(_old), None) => Some(GpuOpacityKeyEvent::Removed(
229                        node_id,
230                        self.opacity_keys.get(&node_id).copied()?,
231                    )),
232                }
233            })
234            .collect::<Vec<GpuOpacityKeyEvent>>();
235
236        // remove / add the opacity keys accordingly
237        for event in all_current_opacity_events.iter() {
238            match &event {
239                GpuOpacityKeyEvent::Added(node_id, key, opacity) => {
240                    self.opacity_keys.insert(*node_id, *key);
241                    self.current_opacity_values.insert(*node_id, *opacity);
242                }
243                GpuOpacityKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
244                    self.current_opacity_values.insert(*node_id, *new_state);
245                }
246                GpuOpacityKeyEvent::Removed(node_id, _key) => {
247                    self.opacity_keys.remove(node_id);
248                    self.current_opacity_values.remove(node_id);
249                }
250            }
251        }
252
253        GpuEventChanges {
254            transform_key_changes: all_current_transform_events,
255            opacity_key_changes: all_current_opacity_events,
256            scrollbar_opacity_changes: Vec::new(), // Filled by separate synchronization
257        }
258    }
259}
260
261/// Represents a change to a scrollbar opacity key.
262///
263/// Scrollbar opacity is managed separately from CSS opacity to enable
264/// independent fading animations without affecting element opacity.
265#[derive(Debug, Clone, PartialEq, PartialOrd)]
266pub enum GpuScrollbarOpacityEvent {
267    /// A vertical scrollbar was added to a node
268    VerticalAdded(DomId, NodeId, OpacityKey, f32),
269    /// A vertical scrollbar opacity was changed
270    VerticalChanged(DomId, NodeId, OpacityKey, f32, f32),
271    /// A vertical scrollbar was removed from a node
272    VerticalRemoved(DomId, NodeId, OpacityKey),
273    /// A horizontal scrollbar was added to a node
274    HorizontalAdded(DomId, NodeId, OpacityKey, f32),
275    /// A horizontal scrollbar opacity was changed
276    HorizontalChanged(DomId, NodeId, OpacityKey, f32, f32),
277    /// A horizontal scrollbar was removed from a node
278    HorizontalRemoved(DomId, NodeId, OpacityKey),
279}
280
281/// Contains all GPU-related change events from a cache synchronization.
282///
283/// This structure groups transform, opacity, and scrollbar opacity changes together
284/// for efficient batch processing when updating WebRender.
285#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
286pub struct GpuEventChanges {
287    /// All transform key changes (additions, modifications, removals)
288    pub transform_key_changes: Vec<GpuTransformKeyEvent>,
289    /// All opacity key changes (additions, modifications, removals)
290    pub opacity_key_changes: Vec<GpuOpacityKeyEvent>,
291    /// All scrollbar opacity key changes (additions, modifications, removals)
292    pub scrollbar_opacity_changes: Vec<GpuScrollbarOpacityEvent>,
293}
294
295impl GpuEventChanges {
296    /// Creates an empty set of GPU event changes.
297    pub fn empty() -> Self {
298        Self::default()
299    }
300
301    /// Returns `true` if there are no transform, opacity, or scrollbar opacity changes.
302    pub fn is_empty(&self) -> bool {
303        self.transform_key_changes.is_empty()
304            && self.opacity_key_changes.is_empty()
305            && self.scrollbar_opacity_changes.is_empty()
306    }
307
308    /// Merges another `GpuEventChanges` into this one, consuming the other.
309    ///
310    /// This is useful for combining changes from multiple sources.
311    pub fn merge(&mut self, other: &mut Self) {
312        self.transform_key_changes
313            .extend(other.transform_key_changes.drain(..));
314        self.opacity_key_changes
315            .extend(other.opacity_key_changes.drain(..));
316        self.scrollbar_opacity_changes
317            .extend(other.scrollbar_opacity_changes.drain(..));
318    }
319}
320
321#[derive(Debug, Clone, PartialEq, PartialOrd)]
322pub enum GpuOpacityKeyEvent {
323    Added(NodeId, OpacityKey, f32),
324    Changed(NodeId, OpacityKey, f32, f32),
325    Removed(NodeId, OpacityKey),
326}