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