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