Skip to main content

fret_ui/tree/ui_tree_invalidation_walk/
propagate.rs

1use super::super::*;
2
3impl<H: UiHost> UiTree<H> {
4    /// Convenience helper for single-window/single-tree setups.
5    ///
6    /// This drains pending model changes from the host and immediately propagates them into the
7    /// tree. Multi-window runtimes should drain `take_changed_models()` once per frame and fan the
8    /// resulting list out to each window's [`UiTree`] instead.
9    pub fn propagate_pending_model_changes(&mut self, app: &mut H) -> bool {
10        let changed = app.take_changed_models();
11        self.propagate_model_changes(app, &changed)
12    }
13
14    fn propagate_observation_masks(
15        &mut self,
16        app: &mut H,
17        masks: impl IntoIterator<Item = (NodeId, ObservationMask)>,
18        source: UiDebugInvalidationSource,
19    ) -> bool {
20        self.propagation_depth_generation = self.propagation_depth_generation.wrapping_add(1);
21        if self.propagation_depth_generation == 0 {
22            self.propagation_depth_generation = 1;
23            self.propagation_depth_cache.clear();
24        }
25        self.propagation_chain.clear();
26        self.propagation_entries.clear();
27
28        for (node, mask) in masks {
29            if mask.is_empty() || !self.nodes.contains_key(node) {
30                continue;
31            }
32
33            let (strength, inv) = if mask.hit_test {
34                (3, Invalidation::HitTest)
35            } else if mask.layout {
36                (2, Invalidation::Layout)
37            } else if mask.paint {
38                (1, Invalidation::Paint)
39            } else {
40                continue;
41            };
42
43            let depth = propagation_depth::propagation_depth_for(self, node);
44            let key = node.data().as_ffi();
45            self.propagation_entries
46                .push((strength, depth, key, node, inv));
47        }
48
49        if self.propagation_entries.is_empty() {
50            return false;
51        }
52
53        self.propagation_entries.sort_by(|a, b| {
54            // Higher-strength invalidations first to maximize reuse via `visited`.
55            b.0.cmp(&a.0)
56                // Within the same strength, prefer ancestors first to reduce redundant walks.
57                .then(a.1.cmp(&b.1))
58                // Stabilize order for determinism in stats/perf.
59                .then(a.2.cmp(&b.2))
60        });
61
62        let mut did_invalidate = false;
63        let mut visited = std::mem::take(&mut self.invalidation_dedup);
64        visited.begin();
65        let mut entries = std::mem::take(&mut self.propagation_entries);
66        for (_, _, _, node, inv) in entries.drain(..) {
67            self.mark_invalidation_dedup_with_source(node, inv, &mut visited, source);
68            did_invalidate = true;
69        }
70        self.invalidation_dedup = visited;
71        self.propagation_entries = entries;
72
73        if did_invalidate {
74            self.request_redraw_coalesced(app);
75        }
76
77        did_invalidate
78    }
79
80    fn propagate_model_changes_from_elements(&mut self, app: &mut H, changed: &[ModelId]) -> bool {
81        let Some(window) = self.window else {
82            return false;
83        };
84        if changed.is_empty() {
85            return false;
86        }
87
88        let changed: std::collections::HashSet<ModelId> = changed.iter().copied().collect();
89        let frame_id = app.frame_id();
90        let mut combined: HashMap<NodeId, ObservationMask> = HashMap::new();
91
92        app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, _app| {
93            let Some(window_state) = runtime.for_window(window) else {
94                return;
95            };
96            window_state.for_each_observed_model_for_invalidation(
97                frame_id,
98                |element, observations| {
99                    let mut mask = ObservationMask::default();
100                    for (model, inv) in observations {
101                        if changed.contains(model) {
102                            mask.add(*inv);
103                        }
104                    }
105                    if mask.is_empty() {
106                        return;
107                    }
108                    let seeded = window_state.node_entry(element).map(|e| e.node);
109                    let Some(node) =
110                        self.resolve_live_attached_node_for_element_seeded(element, seeded)
111                    else {
112                        return;
113                    };
114                    combined
115                        .entry(node)
116                        .and_modify(|m| *m = m.union(mask))
117                        .or_insert(mask);
118                },
119            );
120        });
121
122        if combined.is_empty() {
123            return false;
124        }
125        self.propagate_observation_masks(app, combined, UiDebugInvalidationSource::ModelChange)
126    }
127
128    fn propagate_global_changes_from_elements(&mut self, app: &mut H, changed: &[TypeId]) -> bool {
129        let Some(window) = self.window else {
130            return false;
131        };
132        if changed.is_empty() {
133            return false;
134        }
135
136        let changed: std::collections::HashSet<TypeId> = changed.iter().copied().collect();
137        let frame_id = app.frame_id();
138        let mut combined: HashMap<NodeId, ObservationMask> = HashMap::new();
139
140        app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, _app| {
141            let Some(window_state) = runtime.for_window(window) else {
142                return;
143            };
144            window_state.for_each_observed_global_for_invalidation(
145                frame_id,
146                |element, observations| {
147                    let mut mask = ObservationMask::default();
148                    for (global, inv) in observations {
149                        if changed.contains(global) {
150                            mask.add(*inv);
151                        }
152                    }
153                    if mask.is_empty() {
154                        return;
155                    }
156                    let seeded = window_state.node_entry(element).map(|e| e.node);
157                    let Some(node) =
158                        self.resolve_live_attached_node_for_element_seeded(element, seeded)
159                    else {
160                        return;
161                    };
162                    combined
163                        .entry(node)
164                        .and_modify(|m| *m = m.union(mask))
165                        .or_insert(mask);
166                },
167            );
168        });
169
170        if combined.is_empty() {
171            return false;
172        }
173        self.propagate_observation_masks(app, combined, UiDebugInvalidationSource::GlobalChange)
174    }
175
176    pub fn propagate_model_changes(&mut self, app: &mut H, changed: &[ModelId]) -> bool {
177        if changed.is_empty() {
178            return false;
179        }
180        let frame_id = app.frame_id();
181        #[cfg(debug_assertions)]
182        self.debug_forbid_propagate_after_declarative_render_root(frame_id);
183        self.begin_debug_frame_if_needed(frame_id);
184        if self.debug_enabled {
185            self.debug_model_change_hotspots.clear();
186            self.debug_model_change_unobserved.clear();
187        }
188
189        let mut did_invalidate = false;
190
191        if changed.len() == 1 {
192            let model = changed[0];
193            let layout_nodes = self.observed_in_layout.by_model.get(&model);
194            let paint_nodes = self.observed_in_paint.by_model.get(&model);
195            if let (Some(nodes), None) | (None, Some(nodes)) = (layout_nodes, paint_nodes) {
196                // Copy out the observations so we don't hold a borrow across the invalidation walk.
197                let masks: Vec<(NodeId, ObservationMask)> =
198                    nodes.iter().map(|(&n, &m)| (n, m)).collect();
199                if self.debug_enabled {
200                    self.debug_stats.model_change_invalidation_roots =
201                        masks.len().min(u32::MAX as usize) as u32;
202                    self.debug_stats.model_change_models = 1;
203                    self.debug_stats.model_change_observation_edges =
204                        masks.len().min(u32::MAX as usize) as u32;
205                    self.debug_stats.model_change_unobserved_models = 0;
206                    self.debug_model_change_hotspots = vec![UiDebugModelChangeHotspot {
207                        model,
208                        observation_edges: masks.len().min(u32::MAX as usize) as u32,
209                        changed: app.models().debug_last_changed_info_for_id(model),
210                    }];
211                }
212                did_invalidate |= self.propagate_observation_masks(
213                    app,
214                    masks,
215                    UiDebugInvalidationSource::ModelChange,
216                );
217                did_invalidate |= self.propagate_model_changes_from_elements(app, changed);
218                return did_invalidate;
219            }
220        }
221
222        // Avoid rehash spikes: `changed` is usually small while each changed model/global can have
223        // thousands of observation edges.
224        let mut combined_capacity = 0usize;
225        for &model in changed {
226            if let Some(nodes) = self.observed_in_layout.by_model.get(&model) {
227                combined_capacity = combined_capacity.saturating_add(nodes.len());
228            }
229            if let Some(nodes) = self.observed_in_paint.by_model.get(&model) {
230                combined_capacity = combined_capacity.saturating_add(nodes.len());
231            }
232        }
233        combined_capacity = combined_capacity.min(self.nodes.len());
234
235        let mut combined: HashMap<NodeId, ObservationMask> =
236            HashMap::with_capacity(combined_capacity.max(changed.len().saturating_mul(8)));
237        let mut observation_edges_scanned = 0usize;
238        let mut unobserved_models = 0usize;
239        for &model in changed {
240            let mut edges = 0usize;
241            if let Some(nodes) = self.observed_in_layout.by_model.get(&model) {
242                observation_edges_scanned = observation_edges_scanned.saturating_add(nodes.len());
243                edges = edges.saturating_add(nodes.len());
244                for (&node, &mask) in nodes {
245                    combined
246                        .entry(node)
247                        .and_modify(|m| *m = m.union(mask))
248                        .or_insert(mask);
249                }
250            }
251            if let Some(nodes) = self.observed_in_paint.by_model.get(&model) {
252                observation_edges_scanned = observation_edges_scanned.saturating_add(nodes.len());
253                edges = edges.saturating_add(nodes.len());
254                for (&node, &mask) in nodes {
255                    combined
256                        .entry(node)
257                        .and_modify(|m| *m = m.union(mask))
258                        .or_insert(mask);
259                }
260            }
261            if self.debug_enabled && edges > 0 {
262                self.debug_model_change_hotspots
263                    .push(UiDebugModelChangeHotspot {
264                        model,
265                        observation_edges: edges.min(u32::MAX as usize) as u32,
266                        changed: app.models().debug_last_changed_info_for_id(model),
267                    });
268            }
269            if edges == 0 {
270                unobserved_models = unobserved_models.saturating_add(1);
271                if self.debug_enabled {
272                    self.debug_model_change_unobserved
273                        .push(UiDebugModelChangeUnobserved {
274                            model,
275                            created: app.models().debug_created_info_for_id(model),
276                            changed: app.models().debug_last_changed_info_for_id(model),
277                        });
278                }
279            }
280        }
281
282        if self.debug_enabled {
283            self.debug_stats.model_change_invalidation_roots =
284                combined.len().min(u32::MAX as usize) as u32;
285            self.debug_stats.model_change_models = changed.len().min(u32::MAX as usize) as u32;
286            self.debug_stats.model_change_observation_edges =
287                observation_edges_scanned.min(u32::MAX as usize) as u32;
288            self.debug_stats.model_change_unobserved_models =
289                unobserved_models.min(u32::MAX as usize) as u32;
290
291            self.debug_model_change_hotspots
292                .sort_by(|a, b| b.observation_edges.cmp(&a.observation_edges));
293            self.debug_model_change_hotspots.truncate(5);
294
295            self.debug_model_change_unobserved
296                .sort_by(|a, b| a.model.data().as_ffi().cmp(&b.model.data().as_ffi()));
297            self.debug_model_change_unobserved.truncate(5);
298        }
299        did_invalidate |=
300            self.propagate_observation_masks(app, combined, UiDebugInvalidationSource::ModelChange);
301        did_invalidate |= self.propagate_model_changes_from_elements(app, changed);
302        did_invalidate
303    }
304
305    pub fn propagate_global_changes(&mut self, app: &mut H, changed: &[TypeId]) -> bool {
306        if changed.is_empty() {
307            return false;
308        }
309        let frame_id = app.frame_id();
310        #[cfg(debug_assertions)]
311        self.debug_forbid_propagate_after_declarative_render_root(frame_id);
312        self.begin_debug_frame_if_needed(frame_id);
313        if self.debug_enabled {
314            self.debug_global_change_hotspots.clear();
315            self.debug_global_change_unobserved.clear();
316        }
317
318        let mut did_invalidate = false;
319
320        if changed.len() == 1 {
321            let global = changed[0];
322            let layout_nodes = self.observed_globals_in_layout.by_global.get(&global);
323            let paint_nodes = self.observed_globals_in_paint.by_global.get(&global);
324            if let (Some(nodes), None) | (None, Some(nodes)) = (layout_nodes, paint_nodes) {
325                // Copy out the observations so we don't hold a borrow across the invalidation walk.
326                let masks: Vec<(NodeId, ObservationMask)> =
327                    nodes.iter().map(|(&n, &m)| (n, m)).collect();
328                if self.debug_enabled {
329                    self.debug_stats.global_change_invalidation_roots =
330                        masks.len().min(u32::MAX as usize) as u32;
331                    self.debug_stats.global_change_globals = 1;
332                    self.debug_stats.global_change_observation_edges =
333                        masks.len().min(u32::MAX as usize) as u32;
334                    self.debug_stats.global_change_unobserved_globals = 0;
335                }
336                did_invalidate |= self.propagate_observation_masks(
337                    app,
338                    masks,
339                    UiDebugInvalidationSource::GlobalChange,
340                );
341                did_invalidate |= self.propagate_global_changes_from_elements(app, changed);
342                return did_invalidate;
343            }
344        }
345
346        // Avoid rehash spikes: `changed` is usually small while each changed global can have
347        // thousands of observation edges.
348        let mut combined_capacity = 0usize;
349        for &global in changed {
350            if let Some(nodes) = self.observed_globals_in_layout.by_global.get(&global) {
351                combined_capacity = combined_capacity.saturating_add(nodes.len());
352            }
353            if let Some(nodes) = self.observed_globals_in_paint.by_global.get(&global) {
354                combined_capacity = combined_capacity.saturating_add(nodes.len());
355            }
356        }
357        combined_capacity = combined_capacity.min(self.nodes.len());
358
359        let mut combined: HashMap<NodeId, ObservationMask> =
360            HashMap::with_capacity(combined_capacity.max(changed.len().saturating_mul(8)));
361        let mut observation_edges_scanned = 0usize;
362        let mut unobserved_globals = 0usize;
363        for &global in changed {
364            let mut edges = 0usize;
365            if let Some(nodes) = self.observed_globals_in_layout.by_global.get(&global) {
366                observation_edges_scanned = observation_edges_scanned.saturating_add(nodes.len());
367                edges = edges.saturating_add(nodes.len());
368                for (&node, &mask) in nodes {
369                    combined
370                        .entry(node)
371                        .and_modify(|m| *m = m.union(mask))
372                        .or_insert(mask);
373                }
374            }
375            if let Some(nodes) = self.observed_globals_in_paint.by_global.get(&global) {
376                observation_edges_scanned = observation_edges_scanned.saturating_add(nodes.len());
377                edges = edges.saturating_add(nodes.len());
378                for (&node, &mask) in nodes {
379                    combined
380                        .entry(node)
381                        .and_modify(|m| *m = m.union(mask))
382                        .or_insert(mask);
383                }
384            }
385            if self.debug_enabled && edges > 0 {
386                self.debug_global_change_hotspots
387                    .push(UiDebugGlobalChangeHotspot {
388                        global,
389                        observation_edges: edges.min(u32::MAX as usize) as u32,
390                    });
391            }
392            if edges == 0 {
393                unobserved_globals = unobserved_globals.saturating_add(1);
394                if self.debug_enabled {
395                    self.debug_global_change_unobserved
396                        .push(UiDebugGlobalChangeUnobserved { global });
397                }
398            }
399        }
400
401        if self.debug_enabled {
402            self.debug_stats.global_change_invalidation_roots =
403                combined.len().min(u32::MAX as usize) as u32;
404            self.debug_stats.global_change_globals = changed.len().min(u32::MAX as usize) as u32;
405            self.debug_stats.global_change_observation_edges =
406                observation_edges_scanned.min(u32::MAX as usize) as u32;
407            self.debug_stats.global_change_unobserved_globals =
408                unobserved_globals.min(u32::MAX as usize) as u32;
409
410            self.debug_global_change_hotspots
411                .sort_by(|a, b| b.observation_edges.cmp(&a.observation_edges));
412            self.debug_global_change_hotspots.truncate(5);
413
414            self.debug_global_change_unobserved
415                .sort_by_key(|u| type_id_sort_key(u.global));
416            self.debug_global_change_unobserved.truncate(5);
417        }
418        did_invalidate |= self.propagate_observation_masks(
419            app,
420            combined,
421            UiDebugInvalidationSource::GlobalChange,
422        );
423        did_invalidate |= self.propagate_global_changes_from_elements(app, changed);
424        did_invalidate
425    }
426}