Skip to main content

cranpose_app_shell/
shell_frame.rs

1use super::*;
2
3const DEV_OVERLAY_PADDING: f32 = 8.0;
4const DEV_OVERLAY_FONT_SIZE: f32 = 14.0;
5const DEV_OVERLAY_CHAR_WIDTH: f32 = 7.0;
6const DEV_OVERLAY_SEPARATOR: &str = " | ";
7const DEV_OVERLAY_REFRESH_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
8const DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
9
10#[derive(Copy, Clone)]
11enum DispatchInvalidationKind {
12    Pointer,
13    Focus,
14}
15
16fn frame_stage_telemetry_threshold_ms() -> Option<f64> {
17    static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
18    *THRESHOLD_MS.get_or_init(|| {
19        let explicit = std::env::var("CRANPOSE_FRAME_STAGE_TELEMETRY_MS")
20            .ok()
21            .and_then(|value| value.parse::<f64>().ok())
22            .filter(|value| value.is_finite() && *value >= 0.0);
23        explicit.or_else(|| {
24            std::env::var_os("CRANPOSE_FRAME_STAGE_TELEMETRY")
25                .is_some()
26                .then_some(DEFAULT_FRAME_STAGE_TELEMETRY_THRESHOLD_MS)
27        })
28    })
29}
30
31/// The stages of a frame that its telemetry times: start, initial layout,
32/// post-layout recomposition, dispatch and render.
33type FrameStages = cranpose_core::StageTimes<5>;
34
35fn log_frame_stage_telemetry(stages: &FrameStages) {
36    let Some(threshold_ms) = frame_stage_telemetry_threshold_ms() else {
37        return;
38    };
39    let Some(
40        [
41            frame_start,
42            after_initial_layout,
43            after_layout,
44            after_dispatch,
45            after_render,
46        ],
47    ) = stages.times()
48    else {
49        return;
50    };
51    let ms = cranpose_core::stage_ms;
52    let total_ms = ms(frame_start, after_render);
53    if total_ms < threshold_ms {
54        return;
55    }
56    log::warn!(
57        "[frame-stage-telemetry] total_ms={total_ms:.2} layout_ms={:.2} initial_layout_ms={:.2} post_layout_ms={:.2} dispatch_ms={:.2} scene_ms={:.2}",
58        ms(frame_start, after_layout),
59        ms(frame_start, after_initial_layout),
60        ms(after_initial_layout, after_layout),
61        ms(after_layout, after_dispatch),
62        ms(after_dispatch, after_render),
63    );
64}
65
66fn render_phase_dirty_diagnostics_enabled() -> bool {
67    cranpose_core::env_flag!("CRANPOSE_RENDER_PHASE_DIRTY_DIAG")
68}
69
70struct RenderPhaseDirtyDiagnostics<'a> {
71    render_dirty: bool,
72    pointer_dirty: bool,
73    scene_dirty: bool,
74    draw_repass_pending: bool,
75    draw_dirty_nodes: usize,
76    layer_property_nodes: usize,
77    layout_dirty_nodes: usize,
78    moved_nodes: usize,
79    structural_dirty_nodes: usize,
80    partial_dirty_nodes: usize,
81    dirty_node_ids: Option<String>,
82    render_only_dirty: bool,
83    recomposed_this_frame: bool,
84    path: &'a str,
85}
86
87fn log_render_phase_dirty_diagnostics(diagnostics: RenderPhaseDirtyDiagnostics<'_>) {
88    if !render_phase_dirty_diagnostics_enabled() {
89        return;
90    }
91
92    let RenderPhaseDirtyDiagnostics {
93        render_dirty,
94        pointer_dirty,
95        scene_dirty,
96        draw_repass_pending,
97        draw_dirty_nodes,
98        layer_property_nodes,
99        layout_dirty_nodes,
100        moved_nodes,
101        structural_dirty_nodes,
102        partial_dirty_nodes,
103        dirty_node_ids,
104        render_only_dirty,
105        recomposed_this_frame,
106        path,
107    } = diagnostics;
108    if let Some(dirty_node_ids) = dirty_node_ids {
109        log::warn!(
110            "[render-phase-dirty] path={path} render_dirty={render_dirty} pointer_dirty={pointer_dirty} scene_dirty={scene_dirty} draw_repass_pending={draw_repass_pending} draw_dirty_nodes={draw_dirty_nodes} layer_property_nodes={layer_property_nodes} layout_dirty_nodes={layout_dirty_nodes} moved_nodes={moved_nodes} structural_dirty_nodes={structural_dirty_nodes} partial_dirty_nodes={partial_dirty_nodes} render_only_dirty={render_only_dirty} recomposed_this_frame={recomposed_this_frame} ids={dirty_node_ids}",
111        );
112    } else {
113        log::warn!(
114            "[render-phase-dirty] path={path} render_dirty={render_dirty} pointer_dirty={pointer_dirty} scene_dirty={scene_dirty} draw_repass_pending={draw_repass_pending} draw_dirty_nodes={draw_dirty_nodes} layer_property_nodes={layer_property_nodes} layout_dirty_nodes={layout_dirty_nodes} moved_nodes={moved_nodes} structural_dirty_nodes={structural_dirty_nodes} partial_dirty_nodes={partial_dirty_nodes} render_only_dirty={render_only_dirty} recomposed_this_frame={recomposed_this_frame}",
115        );
116    }
117}
118
119#[derive(Clone, Copy)]
120struct FrameDirt {
121    render_dirty: bool,
122    pointer_dirty: bool,
123    attributed: bool,
124    layer_properties_pending: bool,
125    recomposed_this_frame: bool,
126}
127
128struct SurfaceDirt {
129    draw_dirty_nodes: Vec<NodeId>,
130    layer_property_nodes: Vec<NodeId>,
131    layout_dirty_nodes: Vec<NodeId>,
132    moved_nodes: Vec<NodeId>,
133    structural_parents: Vec<NodeId>,
134    partial_dirty_nodes: Vec<NodeId>,
135    draw_repass_pending: bool,
136    render_only_dirty: bool,
137    visual_only_partial_dirty: bool,
138    full_scene_dirty: bool,
139    needs_scene_rebuild: bool,
140}
141
142struct ScopedVisualNodes {
143    draw: Vec<NodeId>,
144    layers: Vec<NodeId>,
145}
146
147impl SurfaceDirt {
148    fn classify(
149        frame: &FrameDirt,
150        scene_dirty: bool,
151        draw_repass_pending: bool,
152        visual_nodes: ScopedVisualNodes,
153        layout: cranpose_ui::GeometrySceneNodes,
154        structural_parents: Vec<NodeId>,
155        mut partial_dirty_nodes: Vec<NodeId>,
156    ) -> Self {
157        let ScopedVisualNodes {
158            draw: draw_dirty_nodes,
159            layers: layer_property_nodes,
160        } = visual_nodes;
161        let cranpose_ui::GeometrySceneNodes {
162            reshaped: layout_dirty_nodes,
163            moved: moved_nodes,
164        } = layout;
165        let structural_dirty = !structural_parents.is_empty();
166        partial_dirty_nodes.clear();
167        partial_dirty_nodes.extend(draw_dirty_nodes.iter().copied());
168        partial_dirty_nodes.extend(layout_dirty_nodes.iter().copied());
169        partial_dirty_nodes.extend(structural_parents.iter().copied());
170        partial_dirty_nodes.sort_unstable();
171        partial_dirty_nodes.dedup();
172
173        // Moved nodes are not part of the partial list: the scene moves them.
174        let layout_changed = !layout_dirty_nodes.is_empty() || !moved_nodes.is_empty();
175        let partial_scene_dirty = !partial_dirty_nodes.is_empty() || !moved_nodes.is_empty();
176        let render_only_dirty = frame.render_dirty
177            && !frame.attributed
178            && !partial_scene_dirty
179            && layer_property_nodes.is_empty()
180            && !draw_repass_pending
181            && !structural_dirty;
182        let visual_only_partial_dirty = (!draw_dirty_nodes.is_empty()
183            || !layer_property_nodes.is_empty())
184            && !layout_changed
185            && !frame.pointer_dirty
186            && !frame.recomposed_this_frame
187            && !structural_dirty;
188        let scoped_scene_dirty = scene_dirty && layout_changed;
189        let full_scene_dirty = scene_dirty && !scoped_scene_dirty && !visual_only_partial_dirty;
190        // A scoped scene change always has nodes, so `partial_scene_dirty`
191        // covers it.
192        let needs_scene_rebuild = full_scene_dirty
193            || partial_scene_dirty
194            || !layer_property_nodes.is_empty()
195            || draw_repass_pending
196            || structural_dirty;
197        Self {
198            draw_dirty_nodes,
199            layer_property_nodes,
200            layout_dirty_nodes,
201            moved_nodes,
202            structural_parents,
203            partial_dirty_nodes,
204            draw_repass_pending,
205            render_only_dirty,
206            visual_only_partial_dirty,
207            full_scene_dirty,
208            needs_scene_rebuild,
209        }
210    }
211
212    fn use_partial_update(&self) -> bool {
213        (!self.partial_dirty_nodes.is_empty()
214            || !self.layer_property_nodes.is_empty()
215            || !self.moved_nodes.is_empty())
216            && !self.render_only_dirty
217            && !self.full_scene_dirty
218    }
219
220    fn use_visual_update(&self) -> bool {
221        self.use_partial_update() && self.visual_only_partial_dirty
222    }
223
224    fn rebuild_path(&self) -> &'static str {
225        if self.use_visual_update() {
226            "visual-update"
227        } else if self.use_partial_update() {
228            "update"
229        } else {
230            "rebuild"
231        }
232    }
233
234    fn log(&self, frame: &FrameDirt, scene_dirty: bool, path: &str) {
235        log_render_phase_dirty_diagnostics(RenderPhaseDirtyDiagnostics {
236            render_dirty: frame.render_dirty,
237            pointer_dirty: frame.pointer_dirty,
238            scene_dirty,
239            draw_repass_pending: self.draw_repass_pending,
240            draw_dirty_nodes: self.draw_dirty_nodes.len(),
241            layer_property_nodes: self.layer_property_nodes.len(),
242            layout_dirty_nodes: self.layout_dirty_nodes.len(),
243            moved_nodes: self.moved_nodes.len(),
244            structural_dirty_nodes: self.structural_parents.len(),
245            partial_dirty_nodes: self.partial_dirty_nodes.len(),
246            dirty_node_ids: render_phase_dirty_diagnostics_enabled().then(|| {
247                format!(
248                    "draw={:?} layers={:?} layout={:?} moved={:?} structural={:?} partial={:?}",
249                    self.draw_dirty_nodes,
250                    self.layer_property_nodes,
251                    self.layout_dirty_nodes,
252                    self.moved_nodes,
253                    self.structural_parents,
254                    self.partial_dirty_nodes,
255                )
256            }),
257            render_only_dirty: self.render_only_dirty,
258            recomposed_this_frame: frame.recomposed_this_frame,
259            path,
260        });
261    }
262}
263
264struct SurfaceFrame {
265    result: FrameUpdateResult,
266    rebuilt: bool,
267}
268
269fn pending_repass_nodes() -> Vec<NodeId> {
270    let mut nodes = if cranpose_ui::has_pending_layout_repasses() {
271        cranpose_ui::pending_layout_repass_nodes_snapshot()
272    } else {
273        Vec::new()
274    };
275    if cranpose_ui::has_pending_measure_repasses() {
276        for node in cranpose_ui::pending_measure_repass_nodes_snapshot() {
277            if !nodes.contains(&node) {
278                nodes.push(node);
279            }
280        }
281    }
282    nodes
283}
284
285fn mark_root_for_layout(applier: &mut MemoryApplier, root: NodeId) {
286    match applier.with_node::<LayoutNode, _>(root, |node| {
287        node.mark_needs_measure();
288        node.mark_needs_layout();
289    }) {
290        Ok(()) | Err(NodeError::Missing { .. }) => {}
291        Err(NodeError::TypeMismatch { .. }) => {
292            let _ = applier.with_node::<SubcomposeLayoutNode, _>(root, |node| {
293                node.mark_needs_measure();
294                node.mark_needs_layout_flag();
295            });
296        }
297        Err(_) => {}
298    }
299}
300
301pub(crate) const IDLE_PREFETCH_MAX_PASSES: usize = 4;
302
303impl<R> AppShell<R>
304where
305    R: Renderer,
306    R::Error: Debug,
307{
308    /// Turns semantics tracking on or off. Turning it on forces a layout pass and marks every
309    /// surface dirty. Turning it off drops the cached semantics trees.
310    pub fn set_semantics_enabled(&mut self, enabled: bool) {
311        if self.app.semantics_enabled == enabled {
312            return;
313        }
314        self.app.semantics_enabled = enabled;
315        self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
316        if enabled {
317            self.app.request_forced_layout_pass();
318            self.mark_all_dirty();
319        } else {
320            for surface in &mut self.surfaces {
321                surface.semantics_tree = None;
322            }
323        }
324    }
325
326    /// Sets how the platform should vote the display's frame rate for this
327    /// app. The default, [`FrameRatePreference::Auto`], mirrors Compose: the
328    /// panel's fastest rate while frames are being produced, no preference
329    /// when the scene is still.
330    pub fn set_frame_rate_preference(&mut self, preference: crate::FrameRatePreference) {
331        self.surfaces[0].frame_rate_preference = preference;
332    }
333
334    /// The app's current display frame-rate preference. Read by platform
335    /// backends each frame; the vote itself is applied by the backend that
336    /// owns the native window.
337    pub fn frame_rate_preference(&self) -> crate::FrameRatePreference {
338        self.surfaces[0].frame_rate_preference
339    }
340
341    pub(crate) fn process_frame(&mut self) -> FrameUpdateResult {
342        let app_context = Rc::clone(&self.app.app_context);
343        app_context.enter(|| self.process_frame_in_context(false))
344    }
345
346    pub(crate) fn process_frame_in_context(
347        &mut self,
348        recomposed_before_frame: bool,
349    ) -> FrameUpdateResult {
350        let mut stages = FrameStages::start(frame_stage_telemetry_threshold_ms().is_some());
351
352        #[cfg(feature = "installed-fonts")]
353        self.apply_installed_fonts();
354        self.sync_window_roots_in_context();
355        self.run_layout_phase_in_context();
356        stages.reached();
357
358        let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
359
360        stages.reached();
361
362        self.run_dispatch_queues();
363        self.update_modal_focus();
364        self.reveal_new_focus();
365
366        stages.reached();
367
368        clear_transient_scroll_motion_contexts();
369
370        let result = self.run_render_phase_in_context(recomposed_this_frame);
371        stages.reached();
372        log_frame_stage_telemetry(&stages);
373        result
374    }
375
376    /// Whether a lazy list waits for [`Self::run_idle_prefetch`].
377    pub fn has_idle_prefetch_work(&self) -> bool {
378        self.app
379            .app_context
380            .enter(cranpose_ui::has_lazy_prefetch_requests)
381    }
382
383    /// Composes lazy list items that frames left beyond their viewports for
384    /// later, in layout passes, at most four of them. A pass runs while
385    /// `deadline`, if any, still leaves room for the recent cost of one full pass
386    /// and `frame_can_start`, asked before each pass, says the next frame
387    /// cannot start yet.
388    /// Call it while waiting for the next frame. Returns whether a pass ran.
389    pub fn run_idle_prefetch(
390        &mut self,
391        deadline: Option<Instant>,
392        mut frame_can_start: impl FnMut(&mut R) -> bool,
393    ) -> bool {
394        let app_context = Rc::clone(&self.app.app_context);
395        app_context.enter(|| {
396            let mut passes = 0;
397            while passes < IDLE_PREFETCH_MAX_PASSES
398                && cranpose_ui::has_lazy_prefetch_requests()
399                && deadline.is_none_or(|deadline| {
400                    Instant::now() + cranpose_ui::lazy_prefetch_pass_cost() <= deadline
401                })
402                && !frame_can_start(self.renderer())
403            {
404                cranpose_ui::with_lazy_prefetch_pass(|| {
405                    cranpose_ui::drain_lazy_prefetch_requests(cranpose_ui::schedule_measure_repass);
406                    self.run_layout_phase_in_context();
407                    cranpose_ui::warm_prefetched_slices(&mut self.app.composition.applier_mut());
408                });
409                passes += 1;
410            }
411            passes > 0
412        })
413    }
414
415    /// Gives every renderer the fonts installed since the last frame, before
416    /// this frame lays text out.
417    #[cfg(feature = "installed-fonts")]
418    fn apply_installed_fonts(&mut self) {
419        let generation = cranpose_render_common::installed_fonts::installed_font_generation();
420        if generation == self.app.installed_fonts {
421            return;
422        }
423        self.app.installed_fonts = generation;
424        for surface in &mut self.surfaces {
425            surface
426                .renderer
427                .apply_installed_fonts(&self.app.app_context);
428        }
429    }
430
431    #[cfg(test)]
432    pub(crate) fn run_layout_phase(&mut self) {
433        let app_context = Rc::clone(&self.app.app_context);
434        app_context.enter(|| self.run_layout_phase_in_context());
435    }
436
437    pub(crate) fn run_layout_phase_in_context(&mut self) {
438        let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
439            || cranpose_ui::has_pending_measure_repasses();
440        let scoped_layout_nodes = pending_repass_nodes();
441
442        let global_layout_invalidation = take_layout_invalidation();
443        let force_layout_pass = self.app.force_layout_pass;
444
445        if global_layout_invalidation {
446            cranpose_ui::layout::invalidate_all_layout_caches();
447            if let Some(root) = self.app.composition.root() {
448                mark_root_for_layout(&mut self.app.composition.applier_mut(), root);
449            }
450            self.app.request_forced_layout_pass();
451        } else if has_scoped_repasses {
452            self.app.request_layout_pass();
453        }
454
455        if !self.app.layout_requested {
456            return;
457        }
458
459        let Some(root) = self.app.composition.root() else {
460            self.reset_layout_snapshots();
461            return;
462        };
463        let viewport_size = self.surfaces[0].viewport_size();
464        let handle = self.app.composition.runtime_handle();
465        let mut applier = self.app.composition.applier_mut();
466        applier.set_runtime_handle(handle);
467
468        let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
469            .unwrap_or_else(|err| {
470                log::warn!("Cannot check layout dirty status for root #{root}: {err}");
471                true
472            });
473        let needs_layout =
474            self.app.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
475        self.app.layout_requested = false;
476        self.app.force_layout_pass = false;
477
478        if !needs_layout {
479            log::trace!("Skipping layout: tree is clean");
480            applier.clear_runtime_handle();
481            return;
482        }
483
484        let measured = cranpose_ui::measure_layout_with_options(
485            &mut applier,
486            root,
487            viewport_size,
488            MeasureLayoutOptions {
489                collect_semantics: false,
490                build_layout_tree: false,
491            },
492        );
493        applier.clear_runtime_handle();
494        drop(applier);
495        match measured {
496            Ok(_measurements) => {
497                self.forget_frame_snapshots();
498                self.record_layout_scene_nodes(
499                    global_layout_invalidation || force_layout_pass,
500                    has_scoped_repasses,
501                    scoped_layout_nodes,
502                );
503            }
504            Err(err) => {
505                log::error!("failed to compute layout: {err}");
506                self.reset_layout_snapshots();
507            }
508        }
509    }
510
511    fn forget_frame_snapshots(&mut self) {
512        self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
513        for surface in &mut self.surfaces {
514            surface.forget_snapshots();
515        }
516    }
517
518    fn reset_layout_snapshots(&mut self) {
519        self.forget_frame_snapshots();
520        for surface in &mut self.surfaces {
521            surface.invalidate_scene_root(surface.root_node(&self.app));
522        }
523        let _ = cranpose_ui::take_geometry_scene_nodes();
524        self.app.layout_requested = false;
525        self.app.force_layout_pass = false;
526    }
527
528    fn record_layout_scene_nodes(
529        &mut self,
530        global: bool,
531        has_scoped_repasses: bool,
532        scoped_layout_nodes: Vec<NodeId>,
533    ) {
534        cranpose_ui::take_geometry_scene_nodes_into(&mut self.geometry_scene_nodes);
535        self.app.content_moved |= !self.geometry_scene_nodes.is_empty();
536        if global {
537            for surface in &mut self.surfaces {
538                surface.invalidate_scene_root(surface.root_node(&self.app));
539            }
540        }
541        // The scene updates the root of a global pass in place and keeps the
542        // layers below it, so the nodes the pass reshaped or moved go to the
543        // scene too: a padding that grew moves its node's children although
544        // no child moved in its parent.
545        let scoped = (has_scoped_repasses && !global)
546            .then_some(scoped_layout_nodes)
547            .into_iter()
548            .flatten();
549        route_nodes_by_surface(
550            &mut self.app,
551            &mut self.surfaces,
552            scoped.chain(self.geometry_scene_nodes.reshaped.iter().copied()),
553            SurfaceDirtyLane::Layout,
554            &mut self.routing_scratch,
555        );
556        route_nodes_by_surface(
557            &mut self.app,
558            &mut self.surfaces,
559            self.geometry_scene_nodes.moved.iter().copied(),
560            SurfaceDirtyLane::Moved,
561            &mut self.routing_scratch,
562        );
563    }
564
565    fn run_post_layout_recomposition(&mut self) -> bool {
566        if !self.app.composition.should_recompose() {
567            return false;
568        }
569
570        let Some(root_key) = self.app.composition.root_key() else {
571            return false;
572        };
573
574        match self
575            .app
576            .composition
577            .reconcile(root_key, &mut *self.app.content)
578        {
579            Ok(changed) => {
580                if !changed {
581                    return false;
582                }
583                self.app.fps_monitor.record_recomposition();
584                self.sync_window_roots_in_context();
585                if self.app.composition_tree_needs_layout() {
586                    self.app.request_layout_pass();
587                    self.run_layout_phase_in_context();
588                }
589                request_render_invalidation();
590                true
591            }
592            Err(NodeError::Missing { id }) => {
593                log::debug!("Post-layout recomposition skipped: node {id} no longer exists");
594                self.app.request_layout_pass();
595                request_render_invalidation();
596                true
597            }
598            Err(err) => {
599                log::error!("post-layout recomposition failed: {err}");
600                self.app.request_layout_pass();
601                request_render_invalidation();
602                true
603            }
604        }
605    }
606
607    fn run_dispatch_queues(&mut self) {
608        if has_pending_pointer_repasses() {
609            let mut applier = self.app.composition.applier_mut();
610            process_pointer_repasses(|node_id| {
611                match clear_dispatch_invalidation(
612                    &mut applier,
613                    node_id,
614                    DispatchInvalidationKind::Pointer,
615                ) {
616                    Ok(true) => {
617                        log::trace!("Cleared pointer repass flag for node #{node_id}");
618                    }
619                    Ok(false) => {}
620                    Err(err) => {
621                        log::debug!("Could not process pointer repass for node #{node_id}: {err}");
622                    }
623                }
624            });
625        }
626
627        if has_pending_focus_invalidations() {
628            let mut applier = self.app.composition.applier_mut();
629            process_focus_invalidations(|node_id| {
630                match clear_dispatch_invalidation(
631                    &mut applier,
632                    node_id,
633                    DispatchInvalidationKind::Focus,
634                ) {
635                    Ok(true) => {
636                        log::trace!("Cleared focus sync flag for node #{node_id}");
637                    }
638                    Ok(false) => {}
639                    Err(err) => {
640                        log::debug!(
641                            "Could not process focus invalidation for node #{node_id}: {err}"
642                        );
643                    }
644                }
645            });
646        }
647
648        self.app.flush_semantics_invalidations();
649    }
650
651    fn take_structural_change_parents(&mut self) {
652        self.pending_dirty_nodes.clear();
653        if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
654            return;
655        }
656        if self.app.composition.root().is_none() {
657            return;
658        }
659        self.app
660            .composition
661            .applier_mut()
662            .take_structural_change_parents_into(&mut self.pending_dirty_nodes);
663        // A child's z index orders it among its siblings, in its parent's
664        // layer: the parent changed as a child list does.
665        let reordered = self.pending_dirty_nodes.len();
666        cranpose_ui::append_z_order_changes(&mut self.pending_dirty_nodes);
667        if self.pending_dirty_nodes.len() == reordered {
668            return;
669        }
670        let mut applier = self.app.composition.applier_mut();
671        for node in &mut self.pending_dirty_nodes[reordered..] {
672            if let Some(parent) = applier.get_mut(*node).ok().and_then(|child| child.parent()) {
673                *node = parent;
674            }
675        }
676    }
677
678    #[cfg(test)]
679    pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
680        let app_context = Rc::clone(&self.app.app_context);
681        app_context.enter(|| self.run_render_phase_in_context(false))
682    }
683
684    fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
685        let inspector_revision = if self.app.inspector_projector.is_some()
686            && self
687                .surfaces
688                .iter()
689                .any(|surface| surface.inspector.state.open)
690        {
691            self.semantics_snapshot_revision()
692        } else {
693            0
694        };
695        cranpose_ui::tick_cursor_blink();
696        let render_dirty = take_render_invalidation();
697        let pointer_dirty = take_pointer_invalidation();
698        take_focus_invalidation();
699        for surface in &mut self.surfaces {
700            surface.scoped_draw_nodes.clear();
701            surface.scoped_layer_property_nodes.clear();
702            surface.structural_scene_nodes.clear();
703        }
704        cranpose_ui::take_draw_repass_nodes_into(&mut self.pending_dirty_nodes);
705        route_nodes_by_surface(
706            &mut self.app,
707            &mut self.surfaces,
708            self.pending_dirty_nodes.iter().copied(),
709            SurfaceDirtyLane::Draw,
710            &mut self.routing_scratch,
711        );
712        cranpose_ui::take_layer_property_repass_nodes_into(&mut self.pending_layer_property_nodes);
713        route_nodes_by_surface(
714            &mut self.app,
715            &mut self.surfaces,
716            self.pending_layer_property_nodes.iter().copied(),
717            SurfaceDirtyLane::LayerProperties,
718            &mut self.routing_scratch,
719        );
720        self.take_structural_change_parents();
721        route_nodes_by_surface(
722            &mut self.app,
723            &mut self.surfaces,
724            self.pending_dirty_nodes.iter().copied(),
725            SurfaceDirtyLane::Structural,
726            &mut self.routing_scratch,
727        );
728        self.pending_dirty_nodes.clear();
729        self.pending_layer_property_nodes.clear();
730        let _ = cranpose_ui::has_focused_field();
731        let attributed = self.surfaces.iter().any(|surface| {
732            !surface.scoped_draw_nodes.is_empty()
733                || !surface.scoped_layer_property_nodes.is_empty()
734                || !surface.structural_scene_nodes.is_empty()
735        }) || self.surfaces.iter().any(|surface| {
736            !surface.scoped_layout_scene_nodes.is_empty()
737                || !surface.scoped_moved_scene_nodes.is_empty()
738        });
739        let layer_properties_pending = self
740            .surfaces
741            .iter()
742            .any(|surface| !surface.scoped_layer_property_nodes.is_empty());
743        let frame = FrameDirt {
744            render_dirty,
745            pointer_dirty,
746            attributed,
747            layer_properties_pending,
748            recomposed_this_frame,
749        };
750
751        let mut result = FrameUpdateResult::default();
752        for surface in &mut self.surfaces {
753            let mut frame = render_surface(&mut self.app, surface, &frame);
754            frame.result.visual_changed |=
755                crate::inspector::refresh(&mut self.app, surface, inspector_revision);
756            surface.last_update = frame.result;
757            surface.frame_owed |= frame.result.visual_changed;
758            result.visual_changed |= frame.result.visual_changed;
759            result.structure_changed |= frame.result.structure_changed;
760            result.content_redrawn |= frame.rebuilt;
761        }
762        if result.content_redrawn {
763            self.prune_stale_draw_observations();
764        }
765        result.content_moved = std::mem::take(&mut self.app.content_moved);
766        result
767    }
768
769    /// Drops the draw observations of nodes no surface's render graph holds
770    /// any more, once every [`OBSERVATION_PRUNE_INTERVAL`] rebuilt frames:
771    /// finding them walks every node of every graph, and an observation
772    /// left behind in between costs no more than a redraw request for a
773    /// node nothing draws.
774    fn prune_stale_draw_observations(&mut self) {
775        self.app.rebuilt_frames_since_observation_prune += 1;
776        if self.app.rebuilt_frames_since_observation_prune < OBSERVATION_PRUNE_INTERVAL {
777            return;
778        }
779        self.app.rebuilt_frames_since_observation_prune = 0;
780        let mut retained_visual_nodes = HashSet::default();
781        let mut any_graph = false;
782        for surface in &mut self.surfaces {
783            any_graph |= surface
784                .renderer
785                .scene()
786                .collect_retained_visual_observation_nodes(&mut surface.retained_visual_nodes);
787            retained_visual_nodes.extend(surface.retained_visual_nodes.iter().copied());
788        }
789        if any_graph {
790            cranpose_ui::prune_draw_observations_to_nodes(&retained_visual_nodes);
791        }
792    }
793}
794
795/// Rebuilt frames between prunes of stale draw observations: once a second
796/// at 60 Hz.
797pub(crate) const OBSERVATION_PRUNE_INTERVAL: u32 = 60;
798
799fn render_surface<R>(
800    app: &mut ShellApp,
801    surface: &mut RootSurface<R>,
802    frame: &FrameDirt,
803) -> SurfaceFrame
804where
805    R: Renderer,
806    R::Error: Debug,
807{
808    let draw_dirty_nodes = std::mem::take(&mut surface.scoped_draw_nodes);
809    let layer_property_nodes = std::mem::take(&mut surface.scoped_layer_property_nodes);
810    let structural_parents = std::mem::take(&mut surface.structural_scene_nodes);
811    let draw_repass_pending = !draw_dirty_nodes.is_empty();
812    let mut draw_dirty_nodes = refresh_draw_nodes(app, surface, draw_dirty_nodes);
813    if frame.render_dirty && !frame.layer_properties_pending && !draw_repass_pending {
814        draw_dirty_nodes = refresh_retained_redraw_nodes(app, surface, draw_dirty_nodes);
815    }
816    let layout = cranpose_ui::GeometrySceneNodes {
817        reshaped: std::mem::take(&mut surface.scoped_layout_scene_nodes),
818        moved: std::mem::take(&mut surface.scoped_moved_scene_nodes),
819    };
820    let partial_dirty_nodes = std::mem::take(&mut surface.partial_scene_nodes);
821    let scene_dirty = surface.scene_dirty;
822    let mut dirt = SurfaceDirt::classify(
823        frame,
824        scene_dirty,
825        draw_repass_pending,
826        ScopedVisualNodes {
827            draw: draw_dirty_nodes,
828            layers: layer_property_nodes,
829        },
830        layout,
831        structural_parents,
832        partial_dirty_nodes,
833    );
834
835    if !dirt.needs_scene_rebuild {
836        dirt.log(
837            frame,
838            scene_dirty,
839            if dirt.render_only_dirty {
840                "present-retained"
841            } else {
842                "skip"
843            },
844        );
845        surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
846        surface.scoped_moved_scene_nodes = dirt.moved_nodes;
847        surface.scoped_draw_nodes = dirt.draw_dirty_nodes;
848        surface.scoped_layer_property_nodes = dirt.layer_property_nodes;
849        surface.structural_scene_nodes = dirt.structural_parents;
850        surface.partial_scene_nodes = dirt.partial_dirty_nodes;
851        if dirt.render_only_dirty && app.dev_options.fps_counter {
852            draw_dev_overlay(app, surface);
853        }
854        return SurfaceFrame {
855            result: FrameUpdateResult {
856                visual_changed: dirt.render_only_dirty,
857                ..FrameUpdateResult::default()
858            },
859            rebuilt: false,
860        };
861    }
862
863    surface.scene_dirty = false;
864    let structure_changed = !dirt.render_only_dirty && !dirt.use_visual_update();
865    rebuild_surface_scene(app, surface, frame, scene_dirty, &dirt);
866    if dev_overlay_belongs_on(surface.id, app.dev_options.fps_counter) {
867        draw_dev_overlay(app, surface);
868    }
869    dirt.layout_dirty_nodes.clear();
870    dirt.moved_nodes.clear();
871    surface.scoped_moved_scene_nodes = dirt.moved_nodes;
872    surface.scoped_draw_nodes = dirt.draw_dirty_nodes;
873    surface.scoped_layer_property_nodes = dirt.layer_property_nodes;
874    surface.structural_scene_nodes = dirt.structural_parents;
875    surface.partial_scene_nodes = dirt.partial_dirty_nodes;
876    surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
877    SurfaceFrame {
878        result: FrameUpdateResult {
879            visual_changed: true,
880            structure_changed,
881            ..FrameUpdateResult::default()
882        },
883        rebuilt: true,
884    }
885}
886
887fn rebuild_surface_scene<R>(
888    app: &mut ShellApp,
889    surface: &mut RootSurface<R>,
890    frame: &FrameDirt,
891    scene_dirty: bool,
892    dirt: &SurfaceDirt,
893) where
894    R: Renderer,
895    R::Error: Debug,
896{
897    let viewport_size = surface.viewport_size();
898    let Some(root) = surface.root_node(app) else {
899        surface.renderer.scene_mut().clear();
900        return;
901    };
902    let mut applier = app.composition.applier_mut();
903    dirt.log(frame, scene_dirty, dirt.rebuild_path());
904    let rebuild_result = if dirt.use_visual_update() {
905        surface.renderer.update_visual_scene_from_applier(
906            &mut applier,
907            root,
908            viewport_size,
909            SceneUpdates {
910                content: &dirt.partial_dirty_nodes,
911                layers: &dirt.layer_property_nodes,
912                moved: &dirt.moved_nodes,
913            },
914        )
915    } else if dirt.use_partial_update() {
916        surface.renderer.update_scene_from_applier(
917            &mut applier,
918            root,
919            viewport_size,
920            SceneUpdates {
921                content: &dirt.partial_dirty_nodes,
922                layers: &dirt.layer_property_nodes,
923                moved: &dirt.moved_nodes,
924            },
925        )
926    } else {
927        surface
928            .renderer
929            .rebuild_scene_from_applier(&mut applier, root, viewport_size)
930    };
931    if let Err(err) = rebuild_result {
932        log::error!("renderer rebuild failed: {err:?}");
933        surface.renderer.scene_mut().clear();
934    }
935}
936
937fn dev_overlay_belongs_on(id: RootId, fps_counter: bool) -> bool {
938    fps_counter && matches!(id, RootId::Primary)
939}
940
941fn draw_dev_overlay<R: Renderer>(app: &ShellApp, surface: &mut RootSurface<R>) {
942    let viewport_size = surface.viewport_size();
943    surface.refresh_dev_overlay_text_for_frame_at(app, viewport_size, Instant::now());
944    surface
945        .renderer
946        .draw_dev_overlay(surface.dev_overlay_text.as_str(), viewport_size);
947}
948
949fn refresh_draw_nodes<R: Renderer>(
950    app: &mut ShellApp,
951    surface: &mut RootSurface<R>,
952    mut dirty_nodes: Vec<NodeId>,
953) -> Vec<NodeId> {
954    if dirty_nodes.is_empty() {
955        return dirty_nodes;
956    }
957    let Some(layout_tree) = surface.layout_tree.as_mut() else {
958        return dirty_nodes;
959    };
960
961    dirty_nodes.sort_unstable();
962    dirty_nodes.dedup();
963    let dirty_set: HashSet<NodeId> = dirty_nodes.iter().copied().collect();
964    let mut applier = app.composition.applier_mut();
965    let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
966    refresh_layout_box_data(
967        &mut applier,
968        layout_tree.root_mut(),
969        &refresh_scope,
970        &dirty_set,
971    );
972    dirty_nodes
973}
974
975fn refresh_retained_redraw_nodes<R: Renderer>(
976    app: &mut ShellApp,
977    surface: &mut RootSurface<R>,
978    mut dirty_nodes: Vec<NodeId>,
979) -> Vec<NodeId> {
980    dirty_nodes.clear();
981    let Some(root) = surface.root_node(app) else {
982        return dirty_nodes;
983    };
984    {
985        let mut applier = app.composition.applier_mut();
986        collect_retained_redraw_nodes(
987            &mut applier,
988            root,
989            &mut dirty_nodes,
990            &mut surface.redraw_walk,
991        );
992    }
993    refresh_draw_nodes(app, surface, dirty_nodes)
994}
995
996impl<R: Renderer> RootSurface<R> {
997    fn refresh_dev_overlay_text_for_frame_at(
998        &mut self,
999        app: &ShellApp,
1000        viewport_size: Size,
1001        now: Instant,
1002    ) {
1003        if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
1004            return;
1005        }
1006        self.dev_overlay_text = self.build_dev_overlay_text(app, viewport_size);
1007        self.dev_overlay_last_refresh = Some(now);
1008        self.dev_overlay_viewport = Some(viewport_size);
1009    }
1010
1011    fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
1012        if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
1013            return true;
1014        }
1015
1016        self.dev_overlay_last_refresh.is_none_or(|last| {
1017            now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
1018        })
1019    }
1020
1021    fn build_dev_overlay_text(&mut self, app: &ShellApp, viewport_size: Size) -> String {
1022        self.dev_overlay_controls.clear();
1023
1024        let stats = app.fps_monitor.stats();
1025        // Each field with its rank: a viewport too narrow for the whole line,
1026        // such as a phone browser's, drops the highest ranks first.
1027        let fields = [
1028            (0, format!("{:.0} FPS", stats.fps)),
1029            (3, format!("avg {:.1}ms", stats.avg_ms)),
1030            (2, format!("p95 {:.1}ms", stats.p95_ms)),
1031            (4, format!("max {:.1}ms", stats.max_ms)),
1032            (1, format!("work {:.1}ms", stats.work_avg_ms)),
1033            (5, format!("{} recomp/s", stats.recomps_per_second)),
1034        ];
1035        let line_len = |max_rank: u8| {
1036            let (count, chars) = fields
1037                .iter()
1038                .filter(|(rank, _)| *rank <= max_rank)
1039                .fold((0_usize, 0_usize), |(count, chars), (_, field)| {
1040                    (count + 1, chars + field.len())
1041                });
1042            chars + count.saturating_sub(1) * DEV_OVERLAY_SEPARATOR.len()
1043        };
1044        let fitting_chars = ((viewport_size.width - DEV_OVERLAY_PADDING * 2.0)
1045            / DEV_OVERLAY_CHAR_WIDTH)
1046            .max(0.0) as usize;
1047        let controls_chars = if app.dev_options.frame_pacing_controls {
1048            DEV_OVERLAY_SEPARATOR.len() + frame_pacing_controls_len()
1049        } else {
1050            0
1051        };
1052        let mut max_rank = 5;
1053        while max_rank > 0 && line_len(max_rank) + controls_chars > fitting_chars {
1054            max_rank -= 1;
1055        }
1056        let mut text = String::with_capacity(line_len(max_rank) + controls_chars);
1057        for (_, field) in fields.iter().filter(|(rank, _)| *rank <= max_rank) {
1058            if !text.is_empty() {
1059                text.push_str(DEV_OVERLAY_SEPARATOR);
1060            }
1061            text.push_str(field);
1062        }
1063
1064        if !app.dev_options.frame_pacing_controls {
1065            return text;
1066        }
1067
1068        text.push_str(DEV_OVERLAY_SEPARATOR);
1069        let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
1070        for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
1071            if index > 0 {
1072                text.push(' ');
1073            }
1074            let start = text.len();
1075            if mode == app.dev_options.frame_pacing_mode {
1076                text.push('[');
1077                text.push_str(mode.label());
1078                text.push(']');
1079            } else {
1080                text.push_str(mode.label());
1081            }
1082            controls.push((start, text.len(), mode));
1083        }
1084
1085        let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
1086        let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
1087            .max(DEV_OVERLAY_PADDING);
1088        let overlay_y = DEV_OVERLAY_PADDING;
1089        let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
1090        let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
1091        let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
1092
1093        self.dev_overlay_controls = controls
1094            .into_iter()
1095            .map(|(start, end, mode)| DevOverlayControl {
1096                bounds: Rect {
1097                    x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
1098                    y: text_y - 3.0,
1099                    width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
1100                    height: text_height + 6.0,
1101                },
1102                mode,
1103            })
1104            .collect();
1105
1106        text
1107    }
1108}
1109
1110/// The length of the frame pacing controls: every mode's label, a space
1111/// between labels and brackets around the selected one.
1112fn frame_pacing_controls_len() -> usize {
1113    let labels: usize = FramePacingMode::ALL
1114        .iter()
1115        .map(|mode| mode.label().len())
1116        .sum();
1117    labels + FramePacingMode::ALL.len() - 1 + 2
1118}
1119
1120fn clear_dispatch_invalidation(
1121    applier: &mut MemoryApplier,
1122    node_id: NodeId,
1123    invalidation: DispatchInvalidationKind,
1124) -> Result<bool, NodeError> {
1125    match invalidation {
1126        DispatchInvalidationKind::Pointer => {
1127            match applier.with_node::<LayoutNode, _>(node_id, |node| {
1128                let needs_pointer_pass = node.needs_pointer_pass();
1129                if needs_pointer_pass {
1130                    node.clear_needs_pointer_pass();
1131                }
1132                needs_pointer_pass
1133            }) {
1134                Ok(cleared) => Ok(cleared),
1135                Err(NodeError::TypeMismatch { .. }) => applier
1136                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1137                        let needs_pointer_pass = node.needs_pointer_pass();
1138                        if needs_pointer_pass {
1139                            node.clear_needs_pointer_pass();
1140                        }
1141                        needs_pointer_pass
1142                    }),
1143                Err(err) => Err(err),
1144            }
1145        }
1146        DispatchInvalidationKind::Focus => {
1147            match applier.with_node::<LayoutNode, _>(node_id, |node| {
1148                let needs_focus_sync = node.needs_focus_sync();
1149                if needs_focus_sync {
1150                    node.clear_needs_focus_sync();
1151                }
1152                needs_focus_sync
1153            }) {
1154                Ok(cleared) => Ok(cleared),
1155                Err(NodeError::TypeMismatch { .. }) => applier
1156                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1157                        let needs_focus_sync = node.needs_focus_sync();
1158                        if needs_focus_sync {
1159                            node.clear_needs_focus_sync();
1160                        }
1161                        needs_focus_sync
1162                    }),
1163                Err(err) => Err(err),
1164            }
1165        }
1166    }
1167}
1168
1169pub(crate) fn build_draw_refresh_scope(
1170    applier: &mut MemoryApplier,
1171    dirty_nodes: &HashSet<NodeId>,
1172) -> HashSet<NodeId> {
1173    let mut refresh_scope =
1174        HashSet::with_capacity_and_hasher(dirty_nodes.len(), Default::default());
1175    for &dirty_node in dirty_nodes {
1176        let mut current = Some(dirty_node);
1177        while let Some(node_id) = current {
1178            if !refresh_scope.insert(node_id) {
1179                break;
1180            }
1181            current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
1182        }
1183    }
1184    refresh_scope
1185}
1186
1187/// Collects into `dirty_nodes` the nodes under `root` whose redraw flag no
1188/// draw repass took, clearing the flags. A window root below `root` draws on
1189/// a surface of its own. Each node takes one lookup; `pending` holds the
1190/// nodes still to visit and keeps its storage for the next frame.
1191fn collect_retained_redraw_nodes(
1192    applier: &mut MemoryApplier,
1193    root: NodeId,
1194    dirty_nodes: &mut Vec<NodeId>,
1195    pending: &mut Vec<NodeId>,
1196) {
1197    pending.clear();
1198    pending.push(root);
1199    while let Some(id) = pending.pop() {
1200        let Ok(node) = applier.get_mut(id) else {
1201            continue;
1202        };
1203        if let Some(layout) = node.as_any_mut().downcast_mut::<LayoutNode>() {
1204            if id != root && layout.is_window_root() {
1205                continue;
1206            }
1207            if layout.needs_redraw() {
1208                layout.clear_needs_redraw();
1209                dirty_nodes.push(id);
1210            }
1211            // Reversed, so the first child comes off next and the nodes
1212            // come in tree order.
1213            pending.extend(layout.children.iter().rev());
1214            continue;
1215        }
1216        let mut children = Default::default();
1217        node.collect_children_into(&mut children);
1218        pending.extend(children.iter().rev());
1219        if let Some(subcompose) = node.as_any_mut().downcast_mut::<SubcomposeLayoutNode>()
1220            && subcompose.needs_redraw()
1221        {
1222            subcompose.clear_needs_redraw();
1223            dirty_nodes.push(id);
1224        }
1225    }
1226}
1227
1228fn refresh_layout_box_data(
1229    applier: &mut MemoryApplier,
1230    layout: &mut cranpose_ui::layout::LayoutBox,
1231    refresh_scope: &HashSet<NodeId>,
1232    dirty_nodes: &HashSet<NodeId>,
1233) {
1234    if !refresh_scope.contains(&layout.node_id) {
1235        return;
1236    }
1237
1238    if dirty_nodes.contains(&layout.node_id) {
1239        let refreshed = applier
1240            .with_node::<LayoutNode, _>(layout.node_id, |node| {
1241                node.clear_needs_redraw();
1242                (
1243                    node.modifier.clone(),
1244                    node.resolved_modifiers(),
1245                    node.modifier_slices_snapshot(),
1246                    node.semantics_configuration(),
1247                )
1248            })
1249            .or_else(|_| {
1250                applier.with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1251                    node.clear_needs_redraw();
1252                    (
1253                        node.modifier(),
1254                        node.resolved_modifiers(),
1255                        node.modifier_slices_snapshot(),
1256                        node.semantics_configuration(),
1257                    )
1258                })
1259            });
1260        if let Ok((modifier, resolved_modifiers, slices, semantics)) = refreshed {
1261            layout.node_data.modifier = modifier;
1262            layout.node_data.resolved_modifiers = resolved_modifiers;
1263            layout.node_data.modifier_slices = slices;
1264            layout.node_data.semantics = semantics.map(std::rc::Rc::new);
1265        }
1266    }
1267
1268    for child in &mut layout.children {
1269        refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1270    }
1271}
1272
1273#[cfg(test)]
1274#[path = "tests/shell_frame_tests.rs"]
1275mod tests;