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

            
21
use alloc::vec::Vec;
22
#[cfg(feature = "std")]
23
use std::collections::HashMap;
24
#[cfg(not(feature = "std"))]
25
use alloc::collections::BTreeMap as HashMap;
26
use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
27

            
28
use azul_css::props::style::StyleTransformOrigin;
29

            
30
use 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
/// Caches GPU transform and opacity keys and their current values for all nodes.
38
///
39
/// This cache stores the WebRender keys and computed values for nodes with
40
/// CSS transforms or opacity. It's synchronized with the `StyledDom` to detect
41
/// changes and generate minimal update events.
42
#[derive(Default, Debug, Clone)]
43
pub struct GpuValueCache {
44
    /// Vertical scrollbar thumb transform keys (keyed by scrollable node ID)
45
    pub transform_keys: HashMap<NodeId, TransformKey>,
46
    /// Current vertical scrollbar thumb transform values
47
    pub current_transform_values: HashMap<NodeId, ComputedTransform3D>,
48
    /// Horizontal scrollbar thumb transform keys (keyed by scrollable node ID)
49
    pub h_transform_keys: HashMap<NodeId, TransformKey>,
50
    /// Current horizontal scrollbar thumb transform values
51
    pub h_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
52
    /// CSS transform keys (keyed by node ID) — for CSS `transform` property animation.
53
    /// Separate from scrollbar transform keys to avoid SpatialTreeItemKey collisions.
54
    pub css_transform_keys: HashMap<NodeId, TransformKey>,
55
    /// Current CSS transform values (keyed by node ID)
56
    pub css_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
57
    /// CSS opacity keys (keyed by node ID)
58
    pub opacity_keys: HashMap<NodeId, OpacityKey>,
59
    /// Current CSS opacity values (keyed by node ID)
60
    pub current_opacity_values: HashMap<NodeId, f32>,
61
    /// Vertical scrollbar opacity keys (keyed by DOM ID and scrollable node ID)
62
    pub scrollbar_v_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
63
    /// Horizontal scrollbar opacity keys (keyed by DOM ID and scrollable node ID)
64
    pub scrollbar_h_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
65
    /// Current vertical scrollbar opacity values
66
    pub scrollbar_v_opacity_values: HashMap<(DomId, NodeId), f32>,
67
    /// Current horizontal scrollbar opacity values
68
    pub scrollbar_h_opacity_values: HashMap<(DomId, NodeId), f32>,
69
}
70

            
71
/// Represents a change to a GPU transform key.
72
///
73
/// These events are generated when synchronizing the cache with the `StyledDom`
74
/// and are used to update WebRender's transform state efficiently.
75
#[derive(Debug, Clone, PartialEq, PartialOrd)]
76
pub enum GpuTransformKeyEvent {
77
    /// A new transform was added to a node
78
    Added(NodeId, TransformKey, ComputedTransform3D),
79
    /// An existing transform was modified (includes old and new values)
80
    Changed(
81
        NodeId,
82
        TransformKey,
83
        ComputedTransform3D,
84
        ComputedTransform3D,
85
    ),
86
    /// A transform was removed from a node
87
    Removed(NodeId, TransformKey),
88
}
89

            
90
impl GpuValueCache {
91
    /// Creates an empty GPU value cache.
92
    pub fn empty() -> Self {
93
        Self::default()
94
    }
95

            
96
    /// Synchronizes the cache with the current `StyledDom`, generating change events
97
    /// for CSS transform and opacity additions, modifications, and removals.
98
    #[must_use]
99
2730
    pub fn synchronize(&mut self, styled_dom: &StyledDom) -> GpuEventChanges {
100
2730
        let css_property_cache = styled_dom.get_css_property_cache();
101
2730
        let node_data = styled_dom.node_data.as_container();
102
2730
        let node_states = styled_dom.styled_nodes.as_container();
103

            
104
2730
        let default_transform_origin = StyleTransformOrigin::default();
105

            
106
        #[cfg(target_arch = "x86_64")]
107
        unsafe {
108
2730
            if !INITIALIZED.load(AtomicOrdering::SeqCst) {
109
                use core::arch::x86_64::__cpuid;
110

            
111
378
                let mut cpuid = __cpuid(0);
112
378
                let n_ids = cpuid.eax;
113

            
114
378
                if n_ids > 0 {
115
378
                    // cpuid instruction is present
116
378
                    cpuid = __cpuid(1);
117
378
                    USE_SSE.store((cpuid.edx & (1_u32 << 25)) != 0, AtomicOrdering::SeqCst);
118
378
                    USE_AVX.store((cpuid.ecx & (1_u32 << 28)) != 0, AtomicOrdering::SeqCst);
119
378
                }
120
378
                INITIALIZED.store(true, AtomicOrdering::SeqCst);
121
2352
            }
122
        }
123

            
124
        // calculate the transform values of every single node that has a non-default transform.
125
        //
126
        // GPU fast path: `has_transform` is a single bit in the compact cache.
127
        // The overwhelmingly common case is "no transform set", which now reads one
128
        // byte and bails — no cascade walk. Only nodes that actually have a
129
        // transform pay the slow-walk cost (required to retrieve the parsed value).
130
2730
        let all_current_transform_events = (0..styled_dom.node_data.len())
131
18354
            .filter_map(|node_id| {
132
18354
                let node_id = NodeId::new(node_id);
133
18354
                let styled_node_state = &node_states[node_id].styled_node_state;
134
                // Bit-check short-circuit: only proceed if the node might have a transform.
135
18354
                if styled_node_state.is_normal() {
136
18354
                    if let Some(ref cc) = css_property_cache.compact_cache {
137
                        // M12.7: short-circuit the empty-map get. hashbrown's
138
                        // empty-map probe touches the static empty control-group,
139
                        // which mis-lifts to wasm (out-of-bounds access); the web
140
                        // headless layout uses a fresh (empty) GpuValueCache. An
141
                        // empty map has no entry anyway, and is_empty() is len-based
142
                        // (no probe), so the result is identical on desktop.
143
18354
                        if !cc.has_transform(node_id.index())
144
18228
                            && (self.css_current_transform_values.is_empty()
145
42
                                || self.css_current_transform_values.get(&node_id).is_none())
146
                        {
147
18228
                            return None;
148
126
                        }
149
                    }
150
                }
151
126
                let node_data = &node_data[node_id];
152
126
                let current_transform = css_property_cache
153
126
                    .get_transform(node_data, &node_id, styled_node_state)?
154
126
                    .get_property()
155
126
                    .map(|t| {
156
                        // TODO: look up the parent nodes size properly to resolve animation of
157
                        // transforms with %
158
126
                        let parent_size_width = 0.0;
159
126
                        let parent_size_height = 0.0;
160
126
                        let transform_origin = css_property_cache.get_transform_origin(
161
126
                            node_data,
162
126
                            &node_id,
163
126
                            styled_node_state,
164
                        );
165
126
                        let transform_origin = transform_origin
166
126
                            .as_ref()
167
126
                            .and_then(|o| o.get_property())
168
126
                            .unwrap_or(&default_transform_origin);
169

            
170
126
                        ComputedTransform3D::from_style_transform_vec(
171
126
                            t.as_ref(),
172
126
                            transform_origin,
173
126
                            parent_size_width,
174
126
                            parent_size_height,
175
126
                            RotationMode::ForWebRender,
176
                        )
177
126
                    });
178

            
179
126
                let existing_transform = if self.css_current_transform_values.is_empty() {
180
84
                    None
181
                } else {
182
42
                    self.css_current_transform_values.get(&node_id)
183
                };
184

            
185
126
                match (existing_transform, current_transform) {
186
                    (None, None) => None, // no new transform, no old transform
187
84
                    (None, Some(new)) => Some(GpuTransformKeyEvent::Added(
188
84
                        node_id,
189
84
                        TransformKey::unique(),
190
84
                        new,
191
84
                    )),
192
42
                    (Some(old), Some(new)) => Some(GpuTransformKeyEvent::Changed(
193
42
                        node_id,
194
42
                        self.css_transform_keys.get(&node_id).copied()?,
195
42
                        *old,
196
42
                        new,
197
                    )),
198
                    (Some(_old), None) => Some(GpuTransformKeyEvent::Removed(
199
                        node_id,
200
                        self.css_transform_keys.get(&node_id).copied()?,
201
                    )),
202
                }
203
18354
            })
204
2730
            .collect::<Vec<GpuTransformKeyEvent>>();
205

            
206
        // remove / add the CSS transform keys accordingly
207
2730
        for event in all_current_transform_events.iter() {
208
126
            match &event {
209
84
                GpuTransformKeyEvent::Added(node_id, key, matrix) => {
210
84
                    self.css_transform_keys.insert(*node_id, *key);
211
84
                    self.css_current_transform_values.insert(*node_id, *matrix);
212
84
                }
213
42
                GpuTransformKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
214
42
                    self.css_current_transform_values.insert(*node_id, *new_state);
215
42
                }
216
                GpuTransformKeyEvent::Removed(node_id, _key) => {
217
                    self.css_transform_keys.remove(node_id);
218
                    self.css_current_transform_values.remove(node_id);
219
                }
220
            }
221
        }
222

            
223
        // calculate the opacity of every single node that has a non-default opacity
224
        //
225
        // GPU fast path: compact cache encodes opacity as a single u8. Nodes with
226
        // no author-set opacity (the common case) have `OPACITY_SENTINEL` and
227
        // return immediately — no cascade walk. Only non-default opacities
228
        // generate key events.
229
2730
        let all_current_opacity_events = (0..styled_dom.node_data.len())
230
18354
            .filter_map(|node_id| {
231
18354
                let node_id = NodeId::new(node_id);
232
18354
                let styled_node_state = &node_states[node_id].styled_node_state;
233

            
234
                // Fast-path opacity read via compact cache.
235
18354
                let mut compact_opacity: Option<f32> = None;
236
18354
                if styled_node_state.is_normal() {
237
18354
                    if let Some(ref cc) = css_property_cache.compact_cache {
238
18354
                        let raw = cc.get_opacity_raw(node_id.index());
239
18354
                        compact_opacity = if raw == azul_css::compact_cache::OPACITY_SENTINEL {
240
                            // unset → default (1.0) — bail out unless we had a prior opacity key
241
18228
                            if self.current_opacity_values.get(&node_id).is_none() {
242
18228
                                return None;
243
                            }
244
                            None
245
                        } else {
246
126
                            Some((raw as f32) / 254.0)
247
                        };
248
                    }
249
                }
250

            
251
126
                let node_data = &node_data[node_id];
252
126
                let current_opacity: Option<f32> = if let Some(v) = compact_opacity {
253
                    // Fast path: value already read from compact cache.
254
126
                    Some(v)
255
                } else if styled_node_state.is_normal() && css_property_cache.compact_cache.is_some() {
256
                    // Fast path: sentinel — unset → default (1.0, treated as None here).
257
                    None
258
                } else {
259
                    css_property_cache
260
                        .get_opacity(node_data, &node_id, styled_node_state)?
261
                        .get_property()
262
                        .map(|p| p.inner.normalized())
263
                };
264
126
                let existing_opacity = self.current_opacity_values.get(&node_id);
265

            
266
126
                match (existing_opacity, current_opacity) {
267
                    (None, None) => None, // no new opacity, no old opacity
268
84
                    (None, Some(new)) => Some(GpuOpacityKeyEvent::Added(
269
84
                        node_id,
270
84
                        OpacityKey::unique(),
271
84
                        new,
272
84
                    )),
273
42
                    (Some(old), Some(new)) => Some(GpuOpacityKeyEvent::Changed(
274
42
                        node_id,
275
42
                        self.opacity_keys.get(&node_id).copied()?,
276
42
                        *old,
277
42
                        new,
278
                    )),
279
                    (Some(_old), None) => Some(GpuOpacityKeyEvent::Removed(
280
                        node_id,
281
                        self.opacity_keys.get(&node_id).copied()?,
282
                    )),
283
                }
284
18354
            })
285
2730
            .collect::<Vec<GpuOpacityKeyEvent>>();
286

            
287
        // remove / add the opacity keys accordingly
288
2730
        for event in all_current_opacity_events.iter() {
289
126
            match &event {
290
84
                GpuOpacityKeyEvent::Added(node_id, key, opacity) => {
291
84
                    self.opacity_keys.insert(*node_id, *key);
292
84
                    self.current_opacity_values.insert(*node_id, *opacity);
293
84
                }
294
42
                GpuOpacityKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
295
42
                    self.current_opacity_values.insert(*node_id, *new_state);
296
42
                }
297
                GpuOpacityKeyEvent::Removed(node_id, _key) => {
298
                    self.opacity_keys.remove(node_id);
299
                    self.current_opacity_values.remove(node_id);
300
                }
301
            }
302
        }
303

            
304
2730
        GpuEventChanges {
305
2730
            transform_key_changes: all_current_transform_events,
306
2730
            opacity_key_changes: all_current_opacity_events,
307
2730
            scrollbar_opacity_changes: Vec::new(), // Filled by separate synchronization
308
2730
        }
309
2730
    }
310
}
311

            
312
/// Represents a change to a scrollbar opacity key.
313
///
314
/// Scrollbar opacity is managed separately from CSS opacity to enable
315
/// independent fading animations without affecting element opacity.
316
#[derive(Debug, Clone, PartialEq, PartialOrd)]
317
pub enum GpuScrollbarOpacityEvent {
318
    /// A vertical scrollbar was added to a node
319
    VerticalAdded(DomId, NodeId, OpacityKey, f32),
320
    /// A vertical scrollbar opacity was changed
321
    VerticalChanged(DomId, NodeId, OpacityKey, f32, f32),
322
    /// A vertical scrollbar was removed from a node
323
    VerticalRemoved(DomId, NodeId, OpacityKey),
324
    /// A horizontal scrollbar was added to a node
325
    HorizontalAdded(DomId, NodeId, OpacityKey, f32),
326
    /// A horizontal scrollbar opacity was changed
327
    HorizontalChanged(DomId, NodeId, OpacityKey, f32, f32),
328
    /// A horizontal scrollbar was removed from a node
329
    HorizontalRemoved(DomId, NodeId, OpacityKey),
330
}
331

            
332
/// Contains all GPU-related change events from a cache synchronization.
333
///
334
/// This structure groups transform, opacity, and scrollbar opacity changes together
335
/// for efficient batch processing when updating WebRender.
336
#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
337
pub struct GpuEventChanges {
338
    /// All transform key changes (additions, modifications, removals)
339
    pub transform_key_changes: Vec<GpuTransformKeyEvent>,
340
    /// All opacity key changes (additions, modifications, removals)
341
    pub opacity_key_changes: Vec<GpuOpacityKeyEvent>,
342
    /// All scrollbar opacity key changes (additions, modifications, removals)
343
    pub scrollbar_opacity_changes: Vec<GpuScrollbarOpacityEvent>,
344
}
345

            
346
impl GpuEventChanges {
347
    /// Creates an empty set of GPU event changes.
348
5112
    pub fn empty() -> Self {
349
5112
        Self::default()
350
5112
    }
351

            
352
    /// Returns `true` if there are no transform, opacity, or scrollbar opacity changes.
353
    pub fn is_empty(&self) -> bool {
354
        self.transform_key_changes.is_empty()
355
            && self.opacity_key_changes.is_empty()
356
            && self.scrollbar_opacity_changes.is_empty()
357
    }
358

            
359
    /// Merges another `GpuEventChanges` into this one, consuming the other.
360
    ///
361
    /// This is useful for combining changes from multiple sources.
362
2730
    pub fn merge(&mut self, other: &mut Self) {
363
2730
        self.transform_key_changes
364
2730
            .extend(other.transform_key_changes.drain(..));
365
2730
        self.opacity_key_changes
366
2730
            .extend(other.opacity_key_changes.drain(..));
367
2730
        self.scrollbar_opacity_changes
368
2730
            .extend(other.scrollbar_opacity_changes.drain(..));
369
2730
    }
370
}
371

            
372
/// Represents a change to a GPU opacity key.
373
///
374
/// These events are generated when synchronizing the cache with the `StyledDom`
375
/// and are used to update WebRender's opacity state efficiently.
376
#[derive(Debug, Clone, PartialEq, PartialOrd)]
377
pub enum GpuOpacityKeyEvent {
378
    /// A new opacity was added to a node
379
    Added(NodeId, OpacityKey, f32),
380
    /// An existing opacity was modified (includes old and new values)
381
    Changed(NodeId, OpacityKey, f32, f32),
382
    /// An opacity was removed from a node
383
    Removed(NodeId, OpacityKey),
384
}