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;