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
265impl<R> AppShell<R>
266where
267    R: Renderer,
268    R::Error: Debug,
269{
270    pub fn set_semantics_enabled(&mut self, enabled: bool) {
271        if self.app.semantics_enabled == enabled {
272            return;
273        }
274        self.app.semantics_enabled = enabled;
275        self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
276        if enabled {
277            self.app.request_forced_layout_pass();
278            self.mark_all_dirty();
279        } else {
280            for surface in &mut self.surfaces {
281                surface.semantics_tree = None;
282            }
283        }
284    }
285
286    /// Sets how the platform should vote the display's frame rate for this
287    /// app. The default, [`FrameRatePreference::Auto`], mirrors Compose: the
288    /// panel's fastest rate while frames are being produced, no preference
289    /// when the scene is still.
290    pub fn set_frame_rate_preference(&mut self, preference: crate::FrameRatePreference) {
291        self.surfaces[0].frame_rate_preference = preference;
292    }
293
294    /// The app's current display frame-rate preference. Read by platform
295    /// backends each frame; the vote itself is applied by the backend that
296    /// owns the native window.
297    pub fn frame_rate_preference(&self) -> crate::FrameRatePreference {
298        self.surfaces[0].frame_rate_preference
299    }
300
301    pub(crate) fn process_frame(&mut self) -> FrameUpdateResult {
302        let app_context = Rc::clone(&self.app.app_context);
303        app_context.enter(|| self.process_frame_in_context(false))
304    }
305
306    pub(crate) fn process_frame_in_context(
307        &mut self,
308        recomposed_before_frame: bool,
309    ) -> FrameUpdateResult {
310        let frame_start = Instant::now();
311
312        self.sync_window_roots_in_context();
313        self.run_layout_phase_in_context();
314        let after_initial_layout = Instant::now();
315
316        let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
317
318        let after_layout = Instant::now();
319
320        self.run_dispatch_queues();
321        self.update_modal_focus();
322        self.reveal_new_focus();
323
324        let after_dispatch = Instant::now();
325
326        clear_transient_scroll_motion_contexts();
327
328        let result = self.run_render_phase_in_context(recomposed_this_frame);
329        let after_render = Instant::now();
330        log_frame_stage_telemetry(
331            frame_start,
332            after_initial_layout,
333            after_layout,
334            after_dispatch,
335            after_render,
336        );
337        result
338    }
339
340    #[cfg(test)]
341    pub(crate) fn run_layout_phase(&mut self) {
342        let app_context = Rc::clone(&self.app.app_context);
343        app_context.enter(|| self.run_layout_phase_in_context());
344    }
345
346    pub(crate) fn run_layout_phase_in_context(&mut self) {
347        let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
348            || cranpose_ui::has_pending_measure_repasses();
349        let scoped_layout_nodes = pending_repass_nodes();
350
351        let global_layout_invalidation = take_layout_invalidation();
352        let force_layout_pass = self.app.force_layout_pass;
353
354        if global_layout_invalidation {
355            cranpose_ui::layout::invalidate_all_layout_caches();
356            if let Some(root) = self.app.composition.root() {
357                mark_root_for_layout(&mut self.app.composition.applier_mut(), root);
358            }
359            self.app.request_forced_layout_pass();
360        } else if has_scoped_repasses {
361            self.app.request_layout_pass();
362        }
363
364        if !self.app.layout_requested {
365            return;
366        }
367
368        let Some(root) = self.app.composition.root() else {
369            self.reset_layout_snapshots();
370            return;
371        };
372        let viewport_size = self.surfaces[0].viewport_size();
373        let handle = self.app.composition.runtime_handle();
374        let mut applier = self.app.composition.applier_mut();
375        applier.set_runtime_handle(handle);
376
377        let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
378            .unwrap_or_else(|err| {
379                log::warn!(
380                    "Cannot check layout dirty status for root #{}: {}",
381                    root,
382                    err
383                );
384                true
385            });
386        let needs_layout =
387            self.app.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
388        self.app.layout_requested = false;
389        self.app.force_layout_pass = false;
390
391        if !needs_layout {
392            log::trace!("Skipping layout: tree is clean");
393            applier.clear_runtime_handle();
394            return;
395        }
396
397        let measured = cranpose_ui::measure_layout_with_options(
398            &mut applier,
399            root,
400            viewport_size,
401            MeasureLayoutOptions {
402                collect_semantics: false,
403                build_layout_tree: false,
404            },
405        );
406        applier.clear_runtime_handle();
407        drop(applier);
408        match measured {
409            Ok(_measurements) => {
410                self.forget_frame_snapshots();
411                self.record_layout_scene_nodes(
412                    global_layout_invalidation || force_layout_pass,
413                    has_scoped_repasses,
414                    scoped_layout_nodes,
415                );
416            }
417            Err(err) => {
418                log::error!("failed to compute layout: {err}");
419                self.reset_layout_snapshots();
420            }
421        }
422    }
423
424    fn forget_frame_snapshots(&mut self) {
425        self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
426        for surface in &mut self.surfaces {
427            surface.forget_snapshots();
428        }
429    }
430
431    fn reset_layout_snapshots(&mut self) {
432        self.forget_frame_snapshots();
433        for surface in &mut self.surfaces {
434            surface.scoped_layout_scene_nodes.clear();
435            surface.scene_dirty = true;
436        }
437        let _ = cranpose_ui::take_geometry_scene_nodes();
438        self.app.layout_requested = false;
439        self.app.force_layout_pass = false;
440    }
441
442    fn record_layout_scene_nodes(
443        &mut self,
444        global: bool,
445        has_scoped_repasses: bool,
446        scoped_layout_nodes: Vec<NodeId>,
447    ) {
448        if global {
449            for surface in &mut self.surfaces {
450                surface.scoped_layout_scene_nodes.clear();
451                surface.scene_dirty = true;
452            }
453            let _ = cranpose_ui::take_geometry_scene_nodes();
454            return;
455        }
456        let mut nodes = if has_scoped_repasses {
457            scoped_layout_nodes
458        } else {
459            Vec::new()
460        };
461        if let Some(root) = self.app.composition.root() {
462            let mut applier = self.app.composition.applier_mut();
463            for node in cranpose_ui::take_geometry_scene_nodes() {
464                if let Some(node) = applier.scene_node_attached_to(node, root) {
465                    nodes.push(node);
466                }
467            }
468        }
469        let buckets = partition_nodes_by_surface(&mut self.app, &self.surfaces, nodes);
470        let mut any_named = false;
471        for (surface, bucket) in self.surfaces.iter_mut().zip(buckets) {
472            if bucket.is_empty() {
473                continue;
474            }
475            any_named = true;
476            surface.scene_dirty = true;
477            let mut seen: HashSet<NodeId> =
478                surface.scoped_layout_scene_nodes.iter().copied().collect();
479            for node in bucket {
480                if seen.insert(node) {
481                    surface.scoped_layout_scene_nodes.push(node);
482                }
483            }
484        }
485        if !any_named {
486            for surface in &mut self.surfaces {
487                surface.scene_dirty = true;
488            }
489        }
490    }
491
492    fn run_post_layout_recomposition(&mut self) -> bool {
493        if !self.app.composition.should_recompose() {
494            return false;
495        }
496
497        let Some(root_key) = self.app.composition.root_key() else {
498            return false;
499        };
500
501        match self
502            .app
503            .composition
504            .reconcile(root_key, &mut *self.app.content)
505        {
506            Ok(changed) => {
507                if !changed {
508                    return false;
509                }
510                self.app.fps_monitor.record_recomposition();
511                self.sync_window_roots_in_context();
512                if self.app.composition_tree_needs_layout() {
513                    self.app.request_layout_pass();
514                    self.run_layout_phase_in_context();
515                }
516                request_render_invalidation();
517                true
518            }
519            Err(NodeError::Missing { id }) => {
520                log::debug!(
521                    "Post-layout recomposition skipped: node {} no longer exists",
522                    id
523                );
524                self.app.request_layout_pass();
525                request_render_invalidation();
526                true
527            }
528            Err(err) => {
529                log::error!("post-layout recomposition failed: {err}");
530                self.app.request_layout_pass();
531                request_render_invalidation();
532                true
533            }
534        }
535    }
536
537    fn run_dispatch_queues(&mut self) {
538        if has_pending_pointer_repasses() {
539            let mut applier = self.app.composition.applier_mut();
540            process_pointer_repasses(|node_id| {
541                match clear_dispatch_invalidation(
542                    &mut applier,
543                    node_id,
544                    DispatchInvalidationKind::Pointer,
545                ) {
546                    Ok(true) => {
547                        log::trace!("Cleared pointer repass flag for node #{}", node_id);
548                    }
549                    Ok(false) => {}
550                    Err(err) => {
551                        log::debug!(
552                            "Could not process pointer repass for node #{}: {}",
553                            node_id,
554                            err
555                        );
556                    }
557                }
558            });
559        }
560
561        if has_pending_focus_invalidations() {
562            let mut applier = self.app.composition.applier_mut();
563            process_focus_invalidations(|node_id| {
564                match clear_dispatch_invalidation(
565                    &mut applier,
566                    node_id,
567                    DispatchInvalidationKind::Focus,
568                ) {
569                    Ok(true) => {
570                        log::trace!("Cleared focus sync flag for node #{}", node_id);
571                    }
572                    Ok(false) => {}
573                    Err(err) => {
574                        log::debug!(
575                            "Could not process focus invalidation for node #{}: {}",
576                            node_id,
577                            err
578                        );
579                    }
580                }
581            });
582        }
583
584        if has_pending_semantics_invalidations() {
585            let mut applier = self.app.composition.applier_mut();
586            process_semantics_invalidations(|node_id| {
587                cranpose_core::bubble_semantics_dirty(&mut *applier, node_id);
588            });
589        }
590    }
591
592    fn take_structural_change_parents(&mut self) -> Vec<NodeId> {
593        if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
594            return Vec::new();
595        }
596        let Some(root) = self.app.composition.root() else {
597            return Vec::new();
598        };
599        self.app
600            .composition
601            .applier_mut()
602            .take_structural_change_parents_attached_to(root)
603    }
604
605    #[cfg(test)]
606    pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
607        let app_context = Rc::clone(&self.app.app_context);
608        app_context.enter(|| self.run_render_phase_in_context(false))
609    }
610
611    fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
612        let inspector_revision = if self.app.inspector_projector.is_some()
613            && self
614                .surfaces
615                .iter()
616                .any(|surface| surface.inspector.state.open)
617        {
618            self.semantics_snapshot_revision()
619        } else {
620            0
621        };
622        cranpose_ui::tick_cursor_blink();
623        let render_dirty = take_render_invalidation();
624        let pointer_dirty = take_pointer_invalidation();
625        take_focus_invalidation();
626        let draw_dirty =
627            partition_nodes_by_surface(&mut self.app, &self.surfaces, take_draw_repass_nodes());
628        let structural = self.take_structural_change_parents();
629        let structural = partition_nodes_by_surface(&mut self.app, &self.surfaces, structural);
630        let _ = cranpose_ui::has_focused_field();
631        let attributed = draw_dirty
632            .iter()
633            .chain(structural.iter())
634            .any(|nodes| !nodes.is_empty())
635            || self
636                .surfaces
637                .iter()
638                .any(|surface| !surface.scoped_layout_scene_nodes.is_empty());
639        let frame = FrameDirt {
640            render_dirty,
641            pointer_dirty,
642            attributed,
643            recomposed_this_frame,
644        };
645
646        let mut result = FrameUpdateResult::default();
647        let mut retained_visual_nodes = HashSet::new();
648        let mut prune_observations = false;
649        for ((surface, draw_dirty), structural) in
650            self.surfaces.iter_mut().zip(draw_dirty).zip(structural)
651        {
652            let mut frame = render_surface(&mut self.app, surface, &frame, draw_dirty, structural);
653            frame.result.visual_changed |=
654                crate::inspector::refresh(&mut self.app, surface, inspector_revision);
655            surface.last_update = frame.result;
656            surface.frame_owed |= frame.result.visual_changed;
657            result.visual_changed |= frame.result.visual_changed;
658            result.structure_changed |= frame.result.structure_changed;
659            if frame.rebuilt
660                && surface
661                    .renderer
662                    .scene()
663                    .collect_retained_visual_observation_nodes(&mut surface.retained_visual_nodes)
664            {
665                prune_observations = true;
666            }
667            retained_visual_nodes.extend(surface.retained_visual_nodes.iter().copied());
668        }
669        if prune_observations {
670            cranpose_ui::prune_draw_observations_to_nodes(&retained_visual_nodes);
671        }
672        result
673    }
674}
675
676fn render_surface<R>(
677    app: &mut ShellApp,
678    surface: &mut RootSurface<R>,
679    frame: &FrameDirt,
680    draw_dirty_nodes: Vec<NodeId>,
681    structural_parents: Vec<NodeId>,
682) -> SurfaceFrame
683where
684    R: Renderer,
685    R::Error: Debug,
686{
687    let draw_repass_pending = !draw_dirty_nodes.is_empty();
688    let mut draw_dirty_nodes = refresh_draw_nodes(app, surface, draw_dirty_nodes);
689    if frame.render_dirty && !draw_repass_pending {
690        draw_dirty_nodes = refresh_retained_redraw_nodes(app, surface);
691    }
692    let layout_dirty_nodes = std::mem::take(&mut surface.scoped_layout_scene_nodes);
693    let scene_dirty = surface.scene_dirty;
694    let dirt = SurfaceDirt::classify(
695        frame,
696        scene_dirty,
697        draw_repass_pending,
698        draw_dirty_nodes,
699        layout_dirty_nodes,
700        structural_parents,
701    );
702
703    if !dirt.needs_scene_rebuild {
704        dirt.log(
705            frame,
706            scene_dirty,
707            if dirt.render_only_dirty {
708                "present-retained"
709            } else {
710                "skip"
711            },
712        );
713        surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
714        if dirt.render_only_dirty && app.dev_options.fps_counter {
715            draw_dev_overlay(app, surface);
716        }
717        return SurfaceFrame {
718            result: FrameUpdateResult {
719                visual_changed: dirt.render_only_dirty,
720                structure_changed: false,
721            },
722            rebuilt: false,
723        };
724    }
725
726    surface.scene_dirty = false;
727    let structure_changed = !dirt.render_only_dirty && !dirt.visual_update_only();
728    rebuild_surface_scene(app, surface, frame, scene_dirty, &dirt);
729    if dev_overlay_belongs_on(surface.id, app.dev_options.fps_counter) {
730        draw_dev_overlay(app, surface);
731    }
732    SurfaceFrame {
733        result: FrameUpdateResult {
734            visual_changed: true,
735            structure_changed,
736        },
737        rebuilt: true,
738    }
739}
740
741fn rebuild_surface_scene<R>(
742    app: &mut ShellApp,
743    surface: &mut RootSurface<R>,
744    frame: &FrameDirt,
745    scene_dirty: bool,
746    dirt: &SurfaceDirt,
747) where
748    R: Renderer,
749    R::Error: Debug,
750{
751    let viewport_size = surface.viewport_size();
752    let Some(root) = surface.root_node(app) else {
753        surface.renderer.scene_mut().clear();
754        return;
755    };
756    let mut applier = app.composition.applier_mut();
757    dirt.log(frame, scene_dirty, dirt.rebuild_path());
758    let rebuild_result = if dirt.use_visual_update() {
759        surface.renderer.update_visual_scene_from_applier(
760            &mut applier,
761            root,
762            viewport_size,
763            &dirt.partial_dirty_nodes,
764        )
765    } else if dirt.use_partial_update() {
766        surface.renderer.update_scene_from_applier(
767            &mut applier,
768            root,
769            viewport_size,
770            &dirt.partial_dirty_nodes,
771        )
772    } else {
773        surface
774            .renderer
775            .rebuild_scene_from_applier(&mut applier, root, viewport_size)
776    };
777    if let Err(err) = rebuild_result {
778        log::error!("renderer rebuild failed: {err:?}");
779        surface.renderer.scene_mut().clear();
780    }
781}
782
783fn dev_overlay_belongs_on(id: RootId, fps_counter: bool) -> bool {
784    fps_counter && matches!(id, RootId::Primary)
785}
786
787fn draw_dev_overlay<R: Renderer>(app: &ShellApp, surface: &mut RootSurface<R>) {
788    let viewport_size = surface.viewport_size();
789    surface.refresh_dev_overlay_text_for_frame_at(app, viewport_size, Instant::now());
790    surface
791        .renderer
792        .draw_dev_overlay(surface.dev_overlay_text.as_str(), viewport_size);
793}
794
795fn refresh_draw_nodes<R: Renderer>(
796    app: &mut ShellApp,
797    surface: &mut RootSurface<R>,
798    dirty_nodes: Vec<NodeId>,
799) -> Vec<NodeId> {
800    if dirty_nodes.is_empty() {
801        return dirty_nodes;
802    }
803    let Some(layout_tree) = surface.layout_tree.as_mut() else {
804        return dirty_nodes;
805    };
806
807    let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
808    let mut applier = app.composition.applier_mut();
809    let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
810    refresh_layout_box_data(
811        &mut applier,
812        layout_tree.root_mut(),
813        &refresh_scope,
814        &dirty_set,
815    );
816    dirty_set.into_iter().collect()
817}
818
819fn refresh_retained_redraw_nodes<R: Renderer>(
820    app: &mut ShellApp,
821    surface: &mut RootSurface<R>,
822) -> Vec<NodeId> {
823    let Some(root) = surface.root_node(app) else {
824        return Vec::new();
825    };
826    let mut dirty_nodes = Vec::new();
827    {
828        let mut applier = app.composition.applier_mut();
829        collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
830    }
831    refresh_draw_nodes(app, surface, dirty_nodes)
832}
833
834impl<R: Renderer> RootSurface<R> {
835    fn refresh_dev_overlay_text_for_frame_at(
836        &mut self,
837        app: &ShellApp,
838        viewport_size: Size,
839        now: Instant,
840    ) {
841        if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
842            return;
843        }
844        self.dev_overlay_text = self.build_dev_overlay_text(app, viewport_size);
845        self.dev_overlay_last_refresh = Some(now);
846        self.dev_overlay_viewport = Some(viewport_size);
847    }
848
849    fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
850        if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
851            return true;
852        }
853
854        self.dev_overlay_last_refresh
855            .map(|last| {
856                now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
857            })
858            .unwrap_or(true)
859    }
860
861    fn build_dev_overlay_text(&mut self, app: &ShellApp, viewport_size: Size) -> String {
862        self.dev_overlay_controls.clear();
863
864        let stats = app.fps_monitor.stats();
865        let mut text = format!(
866            "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
867            stats.fps,
868            stats.avg_ms,
869            stats.p95_ms,
870            stats.max_ms,
871            stats.work_avg_ms,
872            stats.recomps_per_second
873        );
874
875        if !app.dev_options.frame_pacing_controls {
876            return text;
877        }
878
879        text.push_str(" | ");
880        let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
881        for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
882            if index > 0 {
883                text.push(' ');
884            }
885            let start = text.len();
886            if mode == app.dev_options.frame_pacing_mode {
887                text.push('[');
888                text.push_str(mode.label());
889                text.push(']');
890            } else {
891                text.push_str(mode.label());
892            }
893            controls.push((start, text.len(), mode));
894        }
895
896        let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
897        let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
898            .max(DEV_OVERLAY_PADDING);
899        let overlay_y = DEV_OVERLAY_PADDING;
900        let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
901        let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
902        let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
903
904        self.dev_overlay_controls = controls
905            .into_iter()
906            .map(|(start, end, mode)| DevOverlayControl {
907                bounds: Rect {
908                    x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
909                    y: text_y - 3.0,
910                    width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
911                    height: text_height + 6.0,
912                },
913                mode,
914            })
915            .collect();
916
917        text
918    }
919}
920
921fn clear_dispatch_invalidation(
922    applier: &mut MemoryApplier,
923    node_id: NodeId,
924    invalidation: DispatchInvalidationKind,
925) -> Result<bool, NodeError> {
926    match invalidation {
927        DispatchInvalidationKind::Pointer => {
928            match applier.with_node::<LayoutNode, _>(node_id, |node| {
929                let needs_pointer_pass = node.needs_pointer_pass();
930                if needs_pointer_pass {
931                    node.clear_needs_pointer_pass();
932                }
933                needs_pointer_pass
934            }) {
935                Ok(cleared) => Ok(cleared),
936                Err(NodeError::TypeMismatch { .. }) => applier
937                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
938                        let needs_pointer_pass = node.needs_pointer_pass();
939                        if needs_pointer_pass {
940                            node.clear_needs_pointer_pass();
941                        }
942                        needs_pointer_pass
943                    }),
944                Err(err) => Err(err),
945            }
946        }
947        DispatchInvalidationKind::Focus => {
948            match applier.with_node::<LayoutNode, _>(node_id, |node| {
949                let needs_focus_sync = node.needs_focus_sync();
950                if needs_focus_sync {
951                    node.clear_needs_focus_sync();
952                }
953                needs_focus_sync
954            }) {
955                Ok(cleared) => Ok(cleared),
956                Err(NodeError::TypeMismatch { .. }) => applier
957                    .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
958                        let needs_focus_sync = node.needs_focus_sync();
959                        if needs_focus_sync {
960                            node.clear_needs_focus_sync();
961                        }
962                        needs_focus_sync
963                    }),
964                Err(err) => Err(err),
965            }
966        }
967    }
968}
969
970pub(crate) fn build_draw_refresh_scope(
971    applier: &mut MemoryApplier,
972    dirty_nodes: &HashSet<NodeId>,
973) -> HashSet<NodeId> {
974    let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
975    for &dirty_node in dirty_nodes {
976        let mut current = Some(dirty_node);
977        while let Some(node_id) = current {
978            if !refresh_scope.insert(node_id) {
979                break;
980            }
981            current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
982        }
983    }
984    refresh_scope
985}
986
987fn take_needs_redraw(applier: &mut MemoryApplier, node_id: NodeId) -> bool {
988    match applier.with_node::<LayoutNode, _>(node_id, |node| {
989        let needs_redraw = node.needs_redraw();
990        if needs_redraw {
991            node.clear_needs_redraw();
992        }
993        needs_redraw
994    }) {
995        Ok(needs_redraw) => needs_redraw,
996        Err(NodeError::TypeMismatch { .. }) => applier
997            .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
998                let needs_redraw = node.needs_redraw();
999                if needs_redraw {
1000                    node.clear_needs_redraw();
1001                }
1002                needs_redraw
1003            })
1004            .unwrap_or(false),
1005        Err(_) => false,
1006    }
1007}
1008
1009fn collect_retained_redraw_nodes(
1010    applier: &mut MemoryApplier,
1011    root: NodeId,
1012    dirty_nodes: &mut Vec<NodeId>,
1013) {
1014    let mut children = Default::default();
1015    match applier.get_mut(root) {
1016        Ok(node) => node.collect_children_into(&mut children),
1017        Err(_) => return,
1018    }
1019
1020    if take_needs_redraw(applier, root) {
1021        dirty_nodes.push(root);
1022    }
1023
1024    for child in children {
1025        if cranpose_ui::is_window_root(applier, child) {
1026            continue;
1027        }
1028        collect_retained_redraw_nodes(applier, child, dirty_nodes);
1029    }
1030}
1031
1032fn refresh_layout_box_data(
1033    applier: &mut MemoryApplier,
1034    layout: &mut cranpose_ui::layout::LayoutBox,
1035    refresh_scope: &HashSet<NodeId>,
1036    dirty_nodes: &HashSet<NodeId>,
1037) {
1038    if !refresh_scope.contains(&layout.node_id) {
1039        return;
1040    }
1041
1042    if dirty_nodes.contains(&layout.node_id) {
1043        if let Ok((modifier, resolved_modifiers, slices)) =
1044            applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1045                node.clear_needs_redraw();
1046                (
1047                    node.modifier.clone(),
1048                    node.resolved_modifiers(),
1049                    node.modifier_slices_snapshot(),
1050                )
1051            })
1052        {
1053            layout.node_data.modifier = modifier;
1054            layout.node_data.resolved_modifiers = resolved_modifiers;
1055            layout.node_data.modifier_slices = slices;
1056        } else if let Ok((modifier, resolved_modifiers)) = applier
1057            .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1058                node.clear_needs_redraw();
1059                (node.modifier(), node.resolved_modifiers())
1060            })
1061        {
1062            layout.node_data.modifier = modifier.clone();
1063            layout.node_data.resolved_modifiers = resolved_modifiers;
1064            layout.node_data.modifier_slices =
1065                std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1066        }
1067    }
1068
1069    for child in &mut layout.children {
1070        refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1071    }
1072}
1073
1074#[cfg(test)]
1075#[path = "tests/shell_frame_tests.rs"]
1076mod tests;