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::vec::Vec;
22use std::collections::HashMap;
23use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
24
25use azul_css::props::style::StyleTransformOrigin;
26
27use crate::{
28    dom::{DomId, NodeId},
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#[derive(Default, Debug, Clone)]
40pub struct GpuValueCache {
41    /// Vertical scrollbar thumb transform keys (keyed by scrollable node ID)
42    pub transform_keys: HashMap<NodeId, TransformKey>,
43    /// Current vertical scrollbar thumb transform values
44    pub current_transform_values: HashMap<NodeId, ComputedTransform3D>,
45    /// Horizontal scrollbar thumb transform keys (keyed by scrollable node ID)
46    pub h_transform_keys: HashMap<NodeId, TransformKey>,
47    /// Current horizontal scrollbar thumb transform values
48    pub h_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
49    /// CSS transform keys (keyed by node ID) — for CSS `transform` property animation.
50    /// Separate from scrollbar transform keys to avoid SpatialTreeItemKey collisions.
51    pub css_transform_keys: HashMap<NodeId, TransformKey>,
52    /// Current CSS transform values (keyed by node ID)
53    pub css_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
54    /// CSS opacity keys (keyed by node ID)
55    pub opacity_keys: HashMap<NodeId, OpacityKey>,
56    /// Current CSS opacity values (keyed by node ID)
57    pub current_opacity_values: HashMap<NodeId, f32>,
58    /// Vertical scrollbar opacity keys (keyed by DOM ID and scrollable node ID)
59    pub scrollbar_v_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
60    /// Horizontal scrollbar opacity keys (keyed by DOM ID and scrollable node ID)
61    pub scrollbar_h_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
62    /// Current vertical scrollbar opacity values
63    pub scrollbar_v_opacity_values: HashMap<(DomId, NodeId), f32>,
64    /// Current horizontal scrollbar opacity values
65    pub scrollbar_h_opacity_values: HashMap<(DomId, NodeId), f32>,
66}
67
68/// Represents a change to a GPU transform key.
69///
70/// These events are generated when synchronizing the cache with the `StyledDom`
71/// and are used to update WebRender's transform state efficiently.
72#[derive(Debug, Clone, PartialEq, PartialOrd)]
73pub enum GpuTransformKeyEvent {
74    /// A new transform was added to a node
75    Added(NodeId, TransformKey, ComputedTransform3D),
76    /// An existing transform was modified (includes old and new values)
77    Changed(
78        NodeId,
79        TransformKey,
80        ComputedTransform3D,
81        ComputedTransform3D,
82    ),
83    /// A transform was removed from a node
84    Removed(NodeId, TransformKey),
85}
86
87impl GpuValueCache {
88    /// Creates an empty GPU value cache.
89    pub fn empty() -> Self {
90        Self::default()
91    }
92
93    /// Synchronizes the cache with the current `StyledDom`, generating change events
94    /// for CSS transform and opacity additions, modifications, and removals.
95    #[must_use]
96    pub fn synchronize(&mut self, styled_dom: &StyledDom) -> GpuEventChanges {
97        let css_property_cache = styled_dom.get_css_property_cache();
98        let node_data = styled_dom.node_data.as_container();
99        let node_states = styled_dom.styled_nodes.as_container();
100
101        let default_transform_origin = StyleTransformOrigin::default();
102
103        #[cfg(target_arch = "x86_64")]
104        unsafe {
105            if !INITIALIZED.load(AtomicOrdering::SeqCst) {
106                use core::arch::x86_64::__cpuid;
107
108                let mut cpuid = __cpuid(0);
109                let n_ids = cpuid.eax;
110
111                if n_ids > 0 {
112                    // cpuid instruction is present
113                    cpuid = __cpuid(1);
114                    USE_SSE.store((cpuid.edx & (1_u32 << 25)) != 0, AtomicOrdering::SeqCst);
115                    USE_AVX.store((cpuid.ecx & (1_u32 << 28)) != 0, AtomicOrdering::SeqCst);
116                }
117                INITIALIZED.store(true, AtomicOrdering::SeqCst);
118            }
119        }
120
121        // calculate the transform values of every single node that has a non-default transform.
122        //
123        // GPU fast path: `has_transform` is a single bit in the compact cache.
124        // The overwhelmingly common case is "no transform set", which now reads one
125        // byte and bails — no cascade walk. Only nodes that actually have a
126        // transform pay the slow-walk cost (required to retrieve the parsed value).
127        let all_current_transform_events = (0..styled_dom.node_data.len())
128            .filter_map(|node_id| {
129                let node_id = NodeId::new(node_id);
130                let styled_node_state = &node_states[node_id].styled_node_state;
131                // Bit-check short-circuit: only proceed if the node might have a transform.
132                if styled_node_state.is_normal() {
133                    if let Some(ref cc) = css_property_cache.compact_cache {
134                        // M12.7: short-circuit the empty-map get. hashbrown's
135                        // empty-map probe touches the static empty control-group,
136                        // which mis-lifts to wasm (out-of-bounds access); the web
137                        // headless layout uses a fresh (empty) GpuValueCache. An
138                        // empty map has no entry anyway, and is_empty() is len-based
139                        // (no probe), so the result is identical on desktop.
140                        if !cc.has_transform(node_id.index())
141                            && (self.css_current_transform_values.is_empty()
142                                || self.css_current_transform_values.get(&node_id).is_none())
143                        {
144                            return None;
145                        }
146                    }
147                }
148                let node_data = &node_data[node_id];
149                let current_transform = css_property_cache
150                    .get_transform(node_data, &node_id, styled_node_state)?
151                    .get_property()
152                    .map(|t| {
153                        // TODO: look up the parent nodes size properly to resolve animation of
154                        // transforms with %
155                        let parent_size_width = 0.0;
156                        let parent_size_height = 0.0;
157                        let transform_origin = css_property_cache.get_transform_origin(
158                            node_data,
159                            &node_id,
160                            styled_node_state,
161                        );
162                        let transform_origin = transform_origin
163                            .as_ref()
164                            .and_then(|o| o.get_property())
165                            .unwrap_or(&default_transform_origin);
166
167                        ComputedTransform3D::from_style_transform_vec(
168                            t.as_ref(),
169                            transform_origin,
170                            parent_size_width,
171                            parent_size_height,
172                            RotationMode::ForWebRender,
173                        )
174                    });
175
176                let existing_transform = if self.css_current_transform_values.is_empty() {
177                    None
178                } else {
179                    self.css_current_transform_values.get(&node_id)
180                };
181
182                match (existing_transform, current_transform) {
183                    (None, None) => None, // no new transform, no old transform
184                    (None, Some(new)) => Some(GpuTransformKeyEvent::Added(
185                        node_id,
186                        TransformKey::unique(),
187                        new,
188                    )),
189                    (Some(old), Some(new)) => Some(GpuTransformKeyEvent::Changed(
190                        node_id,
191                        self.css_transform_keys.get(&node_id).copied()?,
192                        *old,
193                        new,
194                    )),
195                    (Some(_old), None) => Some(GpuTransformKeyEvent::Removed(
196                        node_id,
197                        self.css_transform_keys.get(&node_id).copied()?,
198                    )),
199                }
200            })
201            .collect::<Vec<GpuTransformKeyEvent>>();
202
203        // remove / add the CSS transform keys accordingly
204        for event in all_current_transform_events.iter() {
205            match &event {
206                GpuTransformKeyEvent::Added(node_id, key, matrix) => {
207                    self.css_transform_keys.insert(*node_id, *key);
208                    self.css_current_transform_values.insert(*node_id, *matrix);
209                }
210                GpuTransformKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
211                    self.css_current_transform_values.insert(*node_id, *new_state);
212                }
213                GpuTransformKeyEvent::Removed(node_id, _key) => {
214                    self.css_transform_keys.remove(node_id);
215                    self.css_current_transform_values.remove(node_id);
216                }
217            }
218        }
219
220        // calculate the opacity of every single node that has a non-default opacity
221        //
222        // GPU fast path: compact cache encodes opacity as a single u8. Nodes with
223        // no author-set opacity (the common case) have `OPACITY_SENTINEL` and
224        // return immediately — no cascade walk. Only non-default opacities
225        // generate key events.
226        let all_current_opacity_events = (0..styled_dom.node_data.len())
227            .filter_map(|node_id| {
228                let node_id = NodeId::new(node_id);
229                let styled_node_state = &node_states[node_id].styled_node_state;
230
231                // Fast-path opacity read via compact cache.
232                let mut compact_opacity: Option<f32> = None;
233                if styled_node_state.is_normal() {
234                    if let Some(ref cc) = css_property_cache.compact_cache {
235                        let raw = cc.get_opacity_raw(node_id.index());
236                        compact_opacity = if raw == azul_css::compact_cache::OPACITY_SENTINEL {
237                            // unset → default (1.0) — bail out unless we had a prior opacity key
238                            if self.current_opacity_values.get(&node_id).is_none() {
239                                return None;
240                            }
241                            None
242                        } else {
243                            Some((raw as f32) / 254.0)
244                        };
245                    }
246                }
247
248                let node_data = &node_data[node_id];
249                let current_opacity: Option<f32> = if let Some(v) = compact_opacity {
250                    // Fast path: value already read from compact cache.
251                    Some(v)
252                } else if styled_node_state.is_normal() && css_property_cache.compact_cache.is_some() {
253                    // Fast path: sentinel — unset → default (1.0, treated as None here).
254                    None
255                } else {
256                    css_property_cache
257                        .get_opacity(node_data, &node_id, styled_node_state)?
258                        .get_property()
259                        .map(|p| p.inner.normalized())
260                };
261                let existing_opacity = self.current_opacity_values.get(&node_id);
262
263                match (existing_opacity, current_opacity) {
264                    (None, None) => None, // no new opacity, no old opacity
265                    (None, Some(new)) => Some(GpuOpacityKeyEvent::Added(
266                        node_id,
267                        OpacityKey::unique(),
268                        new,
269                    )),
270                    (Some(old), Some(new)) => Some(GpuOpacityKeyEvent::Changed(
271                        node_id,
272                        self.opacity_keys.get(&node_id).copied()?,
273                        *old,
274                        new,
275                    )),
276                    (Some(_old), None) => Some(GpuOpacityKeyEvent::Removed(
277                        node_id,
278                        self.opacity_keys.get(&node_id).copied()?,
279                    )),
280                }
281            })
282            .collect::<Vec<GpuOpacityKeyEvent>>();
283
284        // remove / add the opacity keys accordingly
285        for event in all_current_opacity_events.iter() {
286            match &event {
287                GpuOpacityKeyEvent::Added(node_id, key, opacity) => {
288                    self.opacity_keys.insert(*node_id, *key);
289                    self.current_opacity_values.insert(*node_id, *opacity);
290                }
291                GpuOpacityKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
292                    self.current_opacity_values.insert(*node_id, *new_state);
293                }
294                GpuOpacityKeyEvent::Removed(node_id, _key) => {
295                    self.opacity_keys.remove(node_id);
296                    self.current_opacity_values.remove(node_id);
297                }
298            }
299        }
300
301        GpuEventChanges {
302            transform_key_changes: all_current_transform_events,
303            opacity_key_changes: all_current_opacity_events,
304            scrollbar_opacity_changes: Vec::new(), // Filled by separate synchronization
305        }
306    }
307}
308
309/// Represents a change to a scrollbar opacity key.
310///
311/// Scrollbar opacity is managed separately from CSS opacity to enable
312/// independent fading animations without affecting element opacity.
313#[derive(Debug, Clone, PartialEq, PartialOrd)]
314pub enum GpuScrollbarOpacityEvent {
315    /// A vertical scrollbar was added to a node
316    VerticalAdded(DomId, NodeId, OpacityKey, f32),
317    /// A vertical scrollbar opacity was changed
318    VerticalChanged(DomId, NodeId, OpacityKey, f32, f32),
319    /// A vertical scrollbar was removed from a node
320    VerticalRemoved(DomId, NodeId, OpacityKey),
321    /// A horizontal scrollbar was added to a node
322    HorizontalAdded(DomId, NodeId, OpacityKey, f32),
323    /// A horizontal scrollbar opacity was changed
324    HorizontalChanged(DomId, NodeId, OpacityKey, f32, f32),
325    /// A horizontal scrollbar was removed from a node
326    HorizontalRemoved(DomId, NodeId, OpacityKey),
327}
328
329/// Contains all GPU-related change events from a cache synchronization.
330///
331/// This structure groups transform, opacity, and scrollbar opacity changes together
332/// for efficient batch processing when updating WebRender.
333#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
334pub struct GpuEventChanges {
335    /// All transform key changes (additions, modifications, removals)
336    pub transform_key_changes: Vec<GpuTransformKeyEvent>,
337    /// All opacity key changes (additions, modifications, removals)
338    pub opacity_key_changes: Vec<GpuOpacityKeyEvent>,
339    /// All scrollbar opacity key changes (additions, modifications, removals)
340    pub scrollbar_opacity_changes: Vec<GpuScrollbarOpacityEvent>,
341}
342
343impl GpuEventChanges {
344    /// Creates an empty set of GPU event changes.
345    pub fn empty() -> Self {
346        Self::default()
347    }
348
349    /// Returns `true` if there are no transform, opacity, or scrollbar opacity changes.
350    pub fn is_empty(&self) -> bool {
351        self.transform_key_changes.is_empty()
352            && self.opacity_key_changes.is_empty()
353            && self.scrollbar_opacity_changes.is_empty()
354    }
355
356    /// Merges another `GpuEventChanges` into this one, consuming the other.
357    ///
358    /// This is useful for combining changes from multiple sources.
359    pub fn merge(&mut self, other: &mut Self) {
360        self.transform_key_changes
361            .extend(other.transform_key_changes.drain(..));
362        self.opacity_key_changes
363            .extend(other.opacity_key_changes.drain(..));
364        self.scrollbar_opacity_changes
365            .extend(other.scrollbar_opacity_changes.drain(..));
366    }
367}
368
369/// Represents a change to a GPU opacity key.
370///
371/// These events are generated when synchronizing the cache with the `StyledDom`
372/// and are used to update WebRender's opacity state efficiently.
373#[derive(Debug, Clone, PartialEq, PartialOrd)]
374pub enum GpuOpacityKeyEvent {
375    /// A new opacity was added to a node
376    Added(NodeId, OpacityKey, f32),
377    /// An existing opacity was modified (includes old and new values)
378    Changed(NodeId, OpacityKey, f32, f32),
379    /// An opacity was removed from a node
380    Removed(NodeId, OpacityKey),
381}