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