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
21#[cfg(not(feature = "std"))]
22use alloc::collections::BTreeMap as HashMap;
23use alloc::vec::Vec;
24use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
25#[cfg(feature = "std")]
26use std::collections::HashMap;
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/// 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)]
43pub 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    /// ANIMATION transform keys (keyed by node ID).
58    ///
59    /// A separate channel from `css_transform_keys` on purpose. That map is
60    /// OWNED by `synchronize`, which adds and removes entries to match the
61    /// DOM's CSS `transform` property — so an animation writing into it has its
62    /// keys evicted on the very next frame, and the element snaps instead of
63    /// moving. Scrollbar thumbs already have their own channel for the same
64    /// reason; this follows that precedent rather than fighting the cascade for
65    /// one map.
66    pub anim_transform_keys: HashMap<NodeId, TransformKey>,
67    /// Current animation transform values (keyed by node ID).
68    pub anim_current_transform_values: HashMap<NodeId, ComputedTransform3D>,
69    /// Animation opacity keys (keyed by node ID).
70    pub anim_opacity_keys: HashMap<NodeId, OpacityKey>,
71    /// Current animation opacity values (keyed by node ID).
72    pub anim_current_opacity_values: HashMap<NodeId, f32>,
73    /// CSS opacity keys (keyed by node ID)
74    pub opacity_keys: HashMap<NodeId, OpacityKey>,
75    /// Current CSS opacity values (keyed by node ID)
76    pub current_opacity_values: HashMap<NodeId, f32>,
77    /// Vertical scrollbar opacity keys (keyed by DOM ID and scrollable node ID)
78    pub scrollbar_v_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
79    /// Horizontal scrollbar opacity keys (keyed by DOM ID and scrollable node ID)
80    pub scrollbar_h_opacity_keys: HashMap<(DomId, NodeId), OpacityKey>,
81    /// Current vertical scrollbar opacity values
82    pub scrollbar_v_opacity_values: HashMap<(DomId, NodeId), f32>,
83    /// Current horizontal scrollbar opacity values
84    pub scrollbar_h_opacity_values: HashMap<(DomId, NodeId), f32>,
85}
86
87/// Represents a change to a GPU transform key.
88///
89/// These events are generated when synchronizing the cache with the `StyledDom`
90/// and are used to update `WebRender`'s transform state efficiently.
91#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
92pub enum GpuTransformKeyEvent {
93    /// A new transform was added to a node
94    Added(NodeId, TransformKey, ComputedTransform3D),
95    /// An existing transform was modified (includes old and new values)
96    Changed(
97        NodeId,
98        TransformKey,
99        ComputedTransform3D,
100        ComputedTransform3D,
101    ),
102    /// A transform was removed from a node
103    Removed(NodeId, TransformKey),
104}
105
106impl GpuValueCache {
107    /// Creates an empty GPU value cache.
108    #[must_use]
109    pub fn empty() -> Self {
110        Self::default()
111    }
112
113    /// Fingerprint of the KEY POPULATION the display-list builder consumes —
114    /// which nodes carry which transform/opacity keys, and (for the channels
115    /// the builder `zip`s with their value map) whether a value exists.
116    ///
117    /// This exists because the solver's structural-identity display-list cache
118    /// keyed on (root subtree hash, viewport) alone, and the emitted list is
119    /// ALSO a function of this population: `PushReferenceFrame` is emitted for
120    /// a node exactly when it has a key+value pair. Diff-driven animation
121    /// mints its keys AFTER the first layout (First/Last need solved rects),
122    /// so the very next relayout of the unchanged DOM hit the cache and served
123    /// the PRE-KEY display list back — no reference frames, so no GPU damage,
124    /// so the animation was invisible and every subsequent screenshot froze.
125    ///
126    /// Deliberately a population fingerprint, not a value fingerprint: values
127    /// change every animation tick, and serving the cached list across ticks
128    /// is the entire point of routing animation through GPU keys. The hash
129    /// covers exactly the maps the builder reads: css/anim transform keys
130    /// (plus the keysets of their value maps — a key without a value emits
131    /// nothing), scrollbar v/h thumb transform keys, and scrollbar v/h
132    /// opacity keys. In-process comparison only, so hasher stability across
133    /// runs is not required; iteration order is normalised by sorting.
134    #[must_use]
135    pub fn dl_emission_fingerprint(&self) -> u64 {
136        const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
137        const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
138        let mut entries: Vec<(u8, u64, u64)> = Vec::with_capacity(
139            self.css_transform_keys.len()
140                + self.anim_transform_keys.len()
141                + self.css_current_transform_values.len()
142                + self.anim_current_transform_values.len()
143                + self.transform_keys.len()
144                + self.h_transform_keys.len()
145                + self.scrollbar_v_opacity_keys.len()
146                + self.scrollbar_h_opacity_keys.len(),
147        );
148        for (n, k) in &self.css_transform_keys {
149            entries.push((0, n.index() as u64, k.id as u64));
150        }
151        for n in self.css_current_transform_values.keys() {
152            entries.push((1, n.index() as u64, 0));
153        }
154        for (n, k) in &self.anim_transform_keys {
155            entries.push((2, n.index() as u64, k.id as u64));
156        }
157        for n in self.anim_current_transform_values.keys() {
158            entries.push((3, n.index() as u64, 0));
159        }
160        for (n, k) in &self.transform_keys {
161            entries.push((4, n.index() as u64, k.id as u64));
162        }
163        for (n, k) in &self.h_transform_keys {
164            entries.push((5, n.index() as u64, k.id as u64));
165        }
166        for ((d, n), k) in &self.scrollbar_v_opacity_keys {
167            entries.push((6, (d.inner as u64) << 32 | n.index() as u64, k.id as u64));
168        }
169        for ((d, n), k) in &self.scrollbar_h_opacity_keys {
170            entries.push((7, (d.inner as u64) << 32 | n.index() as u64, k.id as u64));
171        }
172        // Animated opacity binds `PushOpacity.opacity_key`, so its population
173        // shapes the emitted list the same way animated transforms do.
174        for (n, k) in &self.anim_opacity_keys {
175            entries.push((8, n.index() as u64, k.id as u64));
176        }
177        for n in self.anim_current_opacity_values.keys() {
178            entries.push((9, n.index() as u64, 0));
179        }
180        entries.sort_unstable();
181        // FNV-1a over the sorted entry words. Hand-rolled because this file
182        // builds under no_std (where `HashMap` above is really `BTreeMap` and
183        // `DefaultHasher` does not exist) — and in-process comparison needs
184        // no cryptographic strength, only sensitivity to every entry.
185        let mut h: u64 = FNV_OFFSET;
186        for (tag, a, b) in entries {
187            for word in [u64::from(tag), a, b] {
188                h ^= word;
189                h = h.wrapping_mul(FNV_PRIME);
190            }
191        }
192        // An empty population must not collide with "no cache entry" sentinels
193        // downstream; FNV_OFFSET is a fine non-zero value for it.
194        h
195    }
196
197    /// Synchronizes the cache with the current `StyledDom`, generating change events
198    /// for CSS transform and opacity additions, modifications, and removals.
199    ///
200    /// Split into read-only `compute_*_events` passes (which diff against the cache)
201    /// and `apply_*_events` passes (which mutate it).
202    #[must_use]
203    pub fn synchronize(&mut self, styled_dom: &StyledDom) -> GpuEventChanges {
204        self.synchronize_with_sizes(styled_dom, &|_| None)
205    }
206
207    /// [`Self::synchronize`] with the nodes' border-box sizes (logical px),
208    /// which is what `transform-origin` and `translate()` percentages
209    /// resolve against (CSS Transforms 1: the element's own box). Layout
210    /// runs AFTER this sync, so callers pass the PREVIOUS pass's sizes —
211    /// exact in steady state — and correct the values once the new sizes
212    /// exist with [`Self::refresh_transform_values`]. With no size source
213    /// a percentage origin resolves to 0, i.e. the top-left corner: that
214    /// was the only behaviour before, and it pivoted every `rotate()` /
215    /// `scale()` under the default `transform-origin: 50% 50%` at the
216    /// corner instead of the centre.
217    #[must_use]
218    pub fn synchronize_with_sizes(
219        &mut self,
220        styled_dom: &StyledDom,
221        node_size: &dyn Fn(NodeId) -> Option<(f32, f32)>,
222    ) -> GpuEventChanges {
223        Self::init_simd_features();
224
225        let transform_key_changes = self.compute_transform_events(styled_dom, node_size);
226        self.apply_transform_events(&transform_key_changes);
227
228        let opacity_key_changes = self.compute_opacity_events(styled_dom);
229        self.apply_opacity_events(&opacity_key_changes);
230
231        GpuEventChanges {
232            transform_key_changes,
233            opacity_key_changes,
234            scrollbar_opacity_changes: Vec::new(), // Filled by separate synchronization
235        }
236    }
237
238    /// One-time CPU feature detection (SSE/AVX) for the transform math fast paths.
239    #[allow(clippy::missing_const_for_fn)] // non-x86_64 body is empty; x86_64 uses atomics
240    fn init_simd_features() {
241        #[cfg(target_arch = "x86_64")]
242        unsafe {
243            if !INITIALIZED.load(AtomicOrdering::SeqCst) {
244                use core::arch::x86_64::__cpuid;
245
246                let mut cpuid = __cpuid(0);
247                let n_ids = cpuid.eax;
248
249                if n_ids > 0 {
250                    // cpuid instruction is present
251                    cpuid = __cpuid(1);
252                    USE_SSE.store((cpuid.edx & (1_u32 << 25)) != 0, AtomicOrdering::SeqCst);
253                    USE_AVX.store((cpuid.ecx & (1_u32 << 28)) != 0, AtomicOrdering::SeqCst);
254                }
255                INITIALIZED.store(true, AtomicOrdering::SeqCst);
256            }
257        }
258    }
259
260    /// Computes CSS-transform change events against the cached values (read-only).
261    /// The node's CSS `transform` as a matrix, with percentages resolved
262    /// against `size` (its border box, logical px); `None` when the node
263    /// has no transform.
264    fn css_transform_of(
265        styled_dom: &StyledDom,
266        node_id: NodeId,
267        size: (f32, f32),
268    ) -> Option<ComputedTransform3D> {
269        let css_property_cache = styled_dom.get_css_property_cache();
270        let node_data = &styled_dom.node_data.as_container()[node_id];
271        let styled_node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
272        let transform_prop =
273            css_property_cache.get_transform(node_data, &node_id, styled_node_state);
274        let t = transform_prop.as_ref().and_then(|v| v.get_property())?;
275        let default_transform_origin = StyleTransformOrigin::default();
276        let transform_origin =
277            css_property_cache.get_transform_origin(node_data, &node_id, styled_node_state);
278        let transform_origin = transform_origin
279            .as_ref()
280            .and_then(|o| o.get_property())
281            .unwrap_or(&default_transform_origin);
282        Some(ComputedTransform3D::from_style_transform_vec(
283            t.as_ref(),
284            transform_origin,
285            size.0,
286            size.1,
287            RotationMode::ForWebRender,
288        ))
289    }
290
291    /// AFTER layout: recompute every cached CSS transform with the nodes'
292    /// real sizes (see [`Self::synchronize_with_sizes`]). Returns how many
293    /// values changed. Both compositors read the LIVE values from this cache
294    /// (the display list's baked matrix is only the fallback for a key
295    /// nothing has published), so a corrected value reaches the screen in
296    /// the same frame.
297    pub fn refresh_transform_values(
298        &mut self,
299        styled_dom: &StyledDom,
300        node_size: &dyn Fn(NodeId) -> Option<(f32, f32)>,
301    ) -> usize {
302        let node_count = styled_dom.node_data.len();
303        let nodes: Vec<NodeId> = self.css_transform_keys.keys().copied().collect();
304        let mut changed = 0;
305        for node_id in nodes {
306            if node_id.index() >= node_count {
307                continue;
308            }
309            let Some(size) = node_size(node_id) else {
310                continue;
311            };
312            let Some(fresh) = Self::css_transform_of(styled_dom, node_id, size) else {
313                continue;
314            };
315            if self.css_current_transform_values.get(&node_id) != Some(&fresh) {
316                self.css_current_transform_values.insert(node_id, fresh);
317                changed += 1;
318            }
319        }
320        changed
321    }
322
323    fn compute_transform_events(
324        &self,
325        styled_dom: &StyledDom,
326        node_size: &dyn Fn(NodeId) -> Option<(f32, f32)>,
327    ) -> Vec<GpuTransformKeyEvent> {
328        let css_property_cache = styled_dom.get_css_property_cache();
329        let node_states = styled_dom.styled_nodes.as_container();
330
331        // calculate the transform values of every single node that has a non-default transform.
332        //
333        // GPU fast path: `has_transform` is a single bit in the compact cache.
334        // The overwhelmingly common case is "no transform set", which now reads one
335        // byte and bails — no cascade walk. Only nodes that actually have a
336        // transform pay the slow-walk cost (required to retrieve the parsed value).
337        let mut events = (0..styled_dom.node_data.len())
338            .filter_map(|node_id| {
339                let node_id = NodeId::new(node_id);
340                let styled_node_state = &node_states[node_id].styled_node_state;
341                // Bit-check short-circuit: only proceed if the node might have a transform.
342                if styled_node_state.is_normal() {
343                    if let Some(ref cc) = css_property_cache.compact_cache {
344                        // M12.7: short-circuit the empty-map get. hashbrown's
345                        // empty-map probe touches the static empty control-group,
346                        // which mis-lifts to wasm (out-of-bounds access); the web
347                        // headless layout uses a fresh (empty) GpuValueCache. An
348                        // empty map has no entry anyway, and is_empty() is len-based
349                        // (no probe), so the result is identical on desktop.
350                        if !cc.has_transform(node_id.index())
351                            && (self.css_current_transform_values.is_empty()
352                                || !self.css_current_transform_values.contains_key(&node_id))
353                        {
354                            return None;
355                        }
356                    }
357                }
358                // `css_transform_of` turns "no transform cascade entry" (the
359                // ordinary case) into `None` rather than skipping the node, so
360                // a node that just LOST its transform still reaches the
361                // `(Some(old), None) => Removed` arm and its cached
362                // TransformKey is evicted. Percentages resolve against the
363                // node's own box — the previous pass's size before layout, 0
364                // (the corner) when no size is known yet.
365                let size = node_size(node_id).unwrap_or((0.0, 0.0));
366                let current_transform = Self::css_transform_of(styled_dom, node_id, size);
367
368                let existing_transform = if self.css_current_transform_values.is_empty() {
369                    None
370                } else {
371                    self.css_current_transform_values.get(&node_id)
372                };
373
374                match (existing_transform, current_transform) {
375                    (None, None) => None, // no new transform, no old transform
376                    (None, Some(new)) => Some(GpuTransformKeyEvent::Added(
377                        node_id,
378                        TransformKey::unique(),
379                        new,
380                    )),
381                    (Some(old), Some(new)) => Some(GpuTransformKeyEvent::Changed(
382                        node_id,
383                        self.css_transform_keys.get(&node_id).copied()?,
384                        *old,
385                        new,
386                    )),
387                    (Some(_old), None) => Some(GpuTransformKeyEvent::Removed(
388                        node_id,
389                        self.css_transform_keys.get(&node_id).copied()?,
390                    )),
391                }
392            })
393            .collect::<Vec<GpuTransformKeyEvent>>();
394
395        // Structural shrink: any cached transform key whose node no longer
396        // exists in the (smaller) DOM is never visited by the loop above, so it
397        // would leak on the GPU. Emit an explicit Removed for those.
398        let node_count = styled_dom.node_data.len();
399        for (node_id, key) in &self.css_transform_keys {
400            if node_id.index() >= node_count {
401                events.push(GpuTransformKeyEvent::Removed(*node_id, *key));
402            }
403        }
404
405        events
406    }
407
408    /// Applies transform key changes (additions/removals) to the cache.
409    fn apply_transform_events(&mut self, events: &[GpuTransformKeyEvent]) {
410        // remove / add the CSS transform keys accordingly
411        for event in events {
412            match &event {
413                GpuTransformKeyEvent::Added(node_id, key, matrix) => {
414                    self.css_transform_keys.insert(*node_id, *key);
415                    self.css_current_transform_values.insert(*node_id, *matrix);
416                }
417                GpuTransformKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
418                    self.css_current_transform_values
419                        .insert(*node_id, *new_state);
420                }
421                GpuTransformKeyEvent::Removed(node_id, _key) => {
422                    self.css_transform_keys.remove(node_id);
423                    self.css_current_transform_values.remove(node_id);
424                }
425            }
426        }
427    }
428
429    /// Computes opacity change events against the cached values (read-only).
430    fn compute_opacity_events(&self, styled_dom: &StyledDom) -> Vec<GpuOpacityKeyEvent> {
431        let css_property_cache = styled_dom.get_css_property_cache();
432        let node_data = styled_dom.node_data.as_container();
433        let node_states = styled_dom.styled_nodes.as_container();
434
435        // calculate the opacity of every single node that has a non-default opacity
436        //
437        // GPU fast path: compact cache encodes opacity as a single u8. Nodes with
438        // no author-set opacity (the common case) have `OPACITY_SENTINEL` and
439        // return immediately — no cascade walk. Only non-default opacities
440        // generate key events.
441        let mut events = (0..styled_dom.node_data.len())
442            .filter_map(|node_id| {
443                let node_id = NodeId::new(node_id);
444                let styled_node_state = &node_states[node_id].styled_node_state;
445
446                // Fast-path opacity read via compact cache.
447                let mut compact_opacity: Option<f32> = None;
448                if styled_node_state.is_normal() {
449                    if let Some(ref cc) = css_property_cache.compact_cache {
450                        let raw = cc.get_opacity_raw(node_id.index());
451                        compact_opacity = if raw == azul_css::compact_cache::OPACITY_SENTINEL {
452                            // unset → default (1.0) — bail out unless we had a prior opacity key
453                            self.current_opacity_values.get(&node_id)?;
454                            None
455                        } else {
456                            Some(f32::from(raw) / 254.0)
457                        };
458                    }
459                }
460
461                let node_data = &node_data[node_id];
462                let current_opacity: Option<f32> = if let Some(v) = compact_opacity {
463                    // Fast path: value already read from compact cache.
464                    Some(v)
465                } else if styled_node_state.is_normal()
466                    && css_property_cache.compact_cache.is_some()
467                {
468                    // Fast path: sentinel — unset → default (1.0, treated as None here).
469                    None
470                } else {
471                    css_property_cache
472                        .get_opacity(node_data, &node_id, styled_node_state)?
473                        .get_property()
474                        .map(|p| p.inner.normalized())
475                };
476                let existing_opacity = self.current_opacity_values.get(&node_id);
477
478                match (existing_opacity, current_opacity) {
479                    (None, None) => None, // no new opacity, no old opacity
480                    (None, Some(new)) => Some(GpuOpacityKeyEvent::Added(
481                        node_id,
482                        OpacityKey::unique(),
483                        new,
484                    )),
485                    (Some(old), Some(new)) => Some(GpuOpacityKeyEvent::Changed(
486                        node_id,
487                        self.opacity_keys.get(&node_id).copied()?,
488                        *old,
489                        new,
490                    )),
491                    (Some(_old), None) => Some(GpuOpacityKeyEvent::Removed(
492                        node_id,
493                        self.opacity_keys.get(&node_id).copied()?,
494                    )),
495                }
496            })
497            .collect::<Vec<GpuOpacityKeyEvent>>();
498
499        // Structural shrink: emit Removed for cached opacity keys whose node no
500        // longer exists in the (smaller) DOM (never visited by the loop above).
501        let node_count = styled_dom.node_data.len();
502        for (node_id, key) in &self.opacity_keys {
503            if node_id.index() >= node_count {
504                events.push(GpuOpacityKeyEvent::Removed(*node_id, *key));
505            }
506        }
507
508        events
509    }
510
511    /// Applies opacity key changes (additions/removals) to the cache.
512    fn apply_opacity_events(&mut self, events: &[GpuOpacityKeyEvent]) {
513        // remove / add the opacity keys accordingly
514        for event in events {
515            match &event {
516                GpuOpacityKeyEvent::Added(node_id, key, opacity) => {
517                    self.opacity_keys.insert(*node_id, *key);
518                    self.current_opacity_values.insert(*node_id, *opacity);
519                }
520                GpuOpacityKeyEvent::Changed(node_id, _key, _old_state, new_state) => {
521                    self.current_opacity_values.insert(*node_id, *new_state);
522                }
523                GpuOpacityKeyEvent::Removed(node_id, _key) => {
524                    self.opacity_keys.remove(node_id);
525                    self.current_opacity_values.remove(node_id);
526                }
527            }
528        }
529    }
530}
531
532/// Represents a change to a scrollbar opacity key.
533///
534/// Scrollbar opacity is managed separately from CSS opacity to enable
535/// independent fading animations without affecting element opacity.
536#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
537pub enum GpuScrollbarOpacityEvent {
538    /// A vertical scrollbar was added to a node
539    VerticalAdded(DomId, NodeId, OpacityKey, f32),
540    /// A vertical scrollbar opacity was changed
541    VerticalChanged(DomId, NodeId, OpacityKey, f32, f32),
542    /// A vertical scrollbar was removed from a node
543    VerticalRemoved(DomId, NodeId, OpacityKey),
544    /// A horizontal scrollbar was added to a node
545    HorizontalAdded(DomId, NodeId, OpacityKey, f32),
546    /// A horizontal scrollbar opacity was changed
547    HorizontalChanged(DomId, NodeId, OpacityKey, f32, f32),
548    /// A horizontal scrollbar was removed from a node
549    HorizontalRemoved(DomId, NodeId, OpacityKey),
550}
551
552/// Contains all GPU-related change events from a cache synchronization.
553///
554/// This structure groups transform, opacity, and scrollbar opacity changes together
555/// for efficient batch processing when updating `WebRender`.
556#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
557pub struct GpuEventChanges {
558    /// All transform key changes (additions, modifications, removals)
559    pub transform_key_changes: Vec<GpuTransformKeyEvent>,
560    /// All opacity key changes (additions, modifications, removals)
561    pub opacity_key_changes: Vec<GpuOpacityKeyEvent>,
562    /// All scrollbar opacity key changes (additions, modifications, removals)
563    pub scrollbar_opacity_changes: Vec<GpuScrollbarOpacityEvent>,
564}
565
566impl GpuEventChanges {
567    /// Creates an empty set of GPU event changes.
568    #[must_use]
569    pub fn empty() -> Self {
570        Self::default()
571    }
572
573    /// Returns `true` if there are no transform, opacity, or scrollbar opacity changes.
574    #[must_use]
575    pub const fn is_empty(&self) -> bool {
576        self.transform_key_changes.is_empty()
577            && self.opacity_key_changes.is_empty()
578            && self.scrollbar_opacity_changes.is_empty()
579    }
580
581    /// Merges another `GpuEventChanges` into this one, consuming the other.
582    ///
583    /// This is useful for combining changes from multiple sources.
584    pub fn merge(&mut self, other: &mut Self) {
585        self.transform_key_changes
586            .append(&mut other.transform_key_changes);
587        self.opacity_key_changes
588            .append(&mut other.opacity_key_changes);
589        self.scrollbar_opacity_changes
590            .append(&mut other.scrollbar_opacity_changes);
591    }
592}
593
594/// Represents a change to a GPU opacity key.
595///
596/// These events are generated when synchronizing the cache with the `StyledDom`
597/// and are used to update `WebRender`'s opacity state efficiently.
598#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
599pub enum GpuOpacityKeyEvent {
600    /// A new opacity was added to a node
601    Added(NodeId, OpacityKey, f32),
602    /// An existing opacity was modified (includes old and new values)
603    Changed(NodeId, OpacityKey, f32, f32),
604    /// An opacity was removed from a node
605    Removed(NodeId, OpacityKey),
606}
607
608#[cfg(test)]
609#[path = "gpu_test.rs"]
610mod gpu_test;