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