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}