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