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