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 run_idle_prefetch(
384 &mut self,
385 deadline: Option<Instant>,
386 mut frame_can_start: impl FnMut(&mut R) -> bool,
387 ) -> bool {
388 let app_context = Rc::clone(&self.app.app_context);
389 app_context.enter(|| {
390 let mut passes = 0;
391 while passes < IDLE_PREFETCH_MAX_PASSES
392 && cranpose_ui::has_lazy_prefetch_requests()
393 && deadline.is_none_or(|deadline| {
394 Instant::now() + cranpose_ui::lazy_prefetch_pass_cost() <= deadline
395 })
396 && !frame_can_start(self.renderer())
397 {
398 cranpose_ui::with_lazy_prefetch_pass(|| {
399 cranpose_ui::drain_lazy_prefetch_requests(cranpose_ui::schedule_measure_repass);
400 self.run_layout_phase_in_context();
401 cranpose_ui::warm_prefetched_slices(&mut self.app.composition.applier_mut());
402 });
403 passes += 1;
404 }
405 passes > 0
406 })
407 }
408
409 #[cfg(feature = "installed-fonts")]
412 fn apply_installed_fonts(&mut self) {
413 let generation = cranpose_render_common::installed_fonts::installed_font_generation();
414 if generation == self.app.installed_fonts {
415 return;
416 }
417 self.app.installed_fonts = generation;
418 for surface in &mut self.surfaces {
419 surface
420 .renderer
421 .apply_installed_fonts(&self.app.app_context);
422 }
423 }
424
425 #[cfg(test)]
426 pub(crate) fn run_layout_phase(&mut self) {
427 let app_context = Rc::clone(&self.app.app_context);
428 app_context.enter(|| self.run_layout_phase_in_context());
429 }
430
431 pub(crate) fn run_layout_phase_in_context(&mut self) {
432 let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
433 || cranpose_ui::has_pending_measure_repasses();
434 let scoped_layout_nodes = pending_repass_nodes();
435
436 let global_layout_invalidation = take_layout_invalidation();
437 let force_layout_pass = self.app.force_layout_pass;
438
439 if global_layout_invalidation {
440 cranpose_ui::layout::invalidate_all_layout_caches();
441 if let Some(root) = self.app.composition.root() {
442 mark_root_for_layout(&mut self.app.composition.applier_mut(), root);
443 }
444 self.app.request_forced_layout_pass();
445 } else if has_scoped_repasses {
446 self.app.request_layout_pass();
447 }
448
449 if !self.app.layout_requested {
450 return;
451 }
452
453 let Some(root) = self.app.composition.root() else {
454 self.reset_layout_snapshots();
455 return;
456 };
457 let viewport_size = self.surfaces[0].viewport_size();
458 let handle = self.app.composition.runtime_handle();
459 let mut applier = self.app.composition.applier_mut();
460 applier.set_runtime_handle(handle);
461
462 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
463 .unwrap_or_else(|err| {
464 log::warn!("Cannot check layout dirty status for root #{root}: {err}");
465 true
466 });
467 let needs_layout =
468 self.app.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
469 self.app.layout_requested = false;
470 self.app.force_layout_pass = false;
471
472 if !needs_layout {
473 log::trace!("Skipping layout: tree is clean");
474 applier.clear_runtime_handle();
475 return;
476 }
477
478 let measured = cranpose_ui::measure_layout_with_options(
479 &mut applier,
480 root,
481 viewport_size,
482 MeasureLayoutOptions {
483 collect_semantics: false,
484 build_layout_tree: false,
485 },
486 );
487 applier.clear_runtime_handle();
488 drop(applier);
489 match measured {
490 Ok(_measurements) => {
491 self.forget_frame_snapshots();
492 self.record_layout_scene_nodes(
493 global_layout_invalidation || force_layout_pass,
494 has_scoped_repasses,
495 scoped_layout_nodes,
496 );
497 }
498 Err(err) => {
499 log::error!("failed to compute layout: {err}");
500 self.reset_layout_snapshots();
501 }
502 }
503 }
504
505 fn forget_frame_snapshots(&mut self) {
506 self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
507 for surface in &mut self.surfaces {
508 surface.forget_snapshots();
509 }
510 }
511
512 fn reset_layout_snapshots(&mut self) {
513 self.forget_frame_snapshots();
514 for surface in &mut self.surfaces {
515 surface.invalidate_scene_root(surface.root_node(&self.app));
516 }
517 let _ = cranpose_ui::take_geometry_scene_nodes();
518 self.app.layout_requested = false;
519 self.app.force_layout_pass = false;
520 }
521
522 fn record_layout_scene_nodes(
523 &mut self,
524 global: bool,
525 has_scoped_repasses: bool,
526 scoped_layout_nodes: Vec<NodeId>,
527 ) {
528 cranpose_ui::take_geometry_scene_nodes_into(&mut self.geometry_scene_nodes);
529 self.app.content_moved |= !self.geometry_scene_nodes.is_empty();
530 if global {
531 for surface in &mut self.surfaces {
532 surface.invalidate_scene_root(surface.root_node(&self.app));
533 }
534 }
535 let scoped = (has_scoped_repasses && !global)
540 .then_some(scoped_layout_nodes)
541 .into_iter()
542 .flatten();
543 route_nodes_by_surface(
544 &mut self.app,
545 &mut self.surfaces,
546 scoped.chain(self.geometry_scene_nodes.reshaped.iter().copied()),
547 SurfaceDirtyLane::Layout,
548 &mut self.routing_scratch,
549 );
550 route_nodes_by_surface(
551 &mut self.app,
552 &mut self.surfaces,
553 self.geometry_scene_nodes.moved.iter().copied(),
554 SurfaceDirtyLane::Moved,
555 &mut self.routing_scratch,
556 );
557 }
558
559 fn run_post_layout_recomposition(&mut self) -> bool {
560 if !self.app.composition.should_recompose() {
561 return false;
562 }
563
564 let Some(root_key) = self.app.composition.root_key() else {
565 return false;
566 };
567
568 match self
569 .app
570 .composition
571 .reconcile(root_key, &mut *self.app.content)
572 {
573 Ok(changed) => {
574 if !changed {
575 return false;
576 }
577 self.app.fps_monitor.record_recomposition();
578 self.sync_window_roots_in_context();
579 if self.app.composition_tree_needs_layout() {
580 self.app.request_layout_pass();
581 self.run_layout_phase_in_context();
582 }
583 request_render_invalidation();
584 true
585 }
586 Err(NodeError::Missing { id }) => {
587 log::debug!("Post-layout recomposition skipped: node {id} no longer exists");
588 self.app.request_layout_pass();
589 request_render_invalidation();
590 true
591 }
592 Err(err) => {
593 log::error!("post-layout recomposition failed: {err}");
594 self.app.request_layout_pass();
595 request_render_invalidation();
596 true
597 }
598 }
599 }
600
601 fn run_dispatch_queues(&mut self) {
602 if has_pending_pointer_repasses() {
603 let mut applier = self.app.composition.applier_mut();
604 process_pointer_repasses(|node_id| {
605 match clear_dispatch_invalidation(
606 &mut applier,
607 node_id,
608 DispatchInvalidationKind::Pointer,
609 ) {
610 Ok(true) => {
611 log::trace!("Cleared pointer repass flag for node #{node_id}");
612 }
613 Ok(false) => {}
614 Err(err) => {
615 log::debug!("Could not process pointer repass for node #{node_id}: {err}");
616 }
617 }
618 });
619 }
620
621 if has_pending_focus_invalidations() {
622 let mut applier = self.app.composition.applier_mut();
623 process_focus_invalidations(|node_id| {
624 match clear_dispatch_invalidation(
625 &mut applier,
626 node_id,
627 DispatchInvalidationKind::Focus,
628 ) {
629 Ok(true) => {
630 log::trace!("Cleared focus sync flag for node #{node_id}");
631 }
632 Ok(false) => {}
633 Err(err) => {
634 log::debug!(
635 "Could not process focus invalidation for node #{node_id}: {err}"
636 );
637 }
638 }
639 });
640 }
641
642 self.app.flush_semantics_invalidations();
643 }
644
645 fn take_structural_change_parents(&mut self) {
646 self.pending_dirty_nodes.clear();
647 if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
648 return;
649 }
650 if self.app.composition.root().is_none() {
651 return;
652 }
653 self.app
654 .composition
655 .applier_mut()
656 .take_structural_change_parents_into(&mut self.pending_dirty_nodes);
657 }
658
659 #[cfg(test)]
660 pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
661 let app_context = Rc::clone(&self.app.app_context);
662 app_context.enter(|| self.run_render_phase_in_context(false))
663 }
664
665 fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
666 let inspector_revision = if self.app.inspector_projector.is_some()
667 && self
668 .surfaces
669 .iter()
670 .any(|surface| surface.inspector.state.open)
671 {
672 self.semantics_snapshot_revision()
673 } else {
674 0
675 };
676 cranpose_ui::tick_cursor_blink();
677 let render_dirty = take_render_invalidation();
678 let pointer_dirty = take_pointer_invalidation();
679 take_focus_invalidation();
680 for surface in &mut self.surfaces {
681 surface.scoped_draw_nodes.clear();
682 surface.scoped_layer_property_nodes.clear();
683 surface.structural_scene_nodes.clear();
684 }
685 cranpose_ui::take_draw_repass_nodes_into(&mut self.pending_dirty_nodes);
686 route_nodes_by_surface(
687 &mut self.app,
688 &mut self.surfaces,
689 self.pending_dirty_nodes.iter().copied(),
690 SurfaceDirtyLane::Draw,
691 &mut self.routing_scratch,
692 );
693 cranpose_ui::take_layer_property_repass_nodes_into(&mut self.pending_layer_property_nodes);
694 route_nodes_by_surface(
695 &mut self.app,
696 &mut self.surfaces,
697 self.pending_layer_property_nodes.iter().copied(),
698 SurfaceDirtyLane::LayerProperties,
699 &mut self.routing_scratch,
700 );
701 self.take_structural_change_parents();
702 route_nodes_by_surface(
703 &mut self.app,
704 &mut self.surfaces,
705 self.pending_dirty_nodes.iter().copied(),
706 SurfaceDirtyLane::Structural,
707 &mut self.routing_scratch,
708 );
709 self.pending_dirty_nodes.clear();
710 self.pending_layer_property_nodes.clear();
711 let _ = cranpose_ui::has_focused_field();
712 let attributed = self.surfaces.iter().any(|surface| {
713 !surface.scoped_draw_nodes.is_empty()
714 || !surface.scoped_layer_property_nodes.is_empty()
715 || !surface.structural_scene_nodes.is_empty()
716 }) || self.surfaces.iter().any(|surface| {
717 !surface.scoped_layout_scene_nodes.is_empty()
718 || !surface.scoped_moved_scene_nodes.is_empty()
719 });
720 let layer_properties_pending = self
721 .surfaces
722 .iter()
723 .any(|surface| !surface.scoped_layer_property_nodes.is_empty());
724 let frame = FrameDirt {
725 render_dirty,
726 pointer_dirty,
727 attributed,
728 layer_properties_pending,
729 recomposed_this_frame,
730 };
731
732 let mut result = FrameUpdateResult::default();
733 for surface in &mut self.surfaces {
734 let mut frame = render_surface(&mut self.app, surface, &frame);
735 frame.result.visual_changed |=
736 crate::inspector::refresh(&mut self.app, surface, inspector_revision);
737 surface.last_update = frame.result;
738 surface.frame_owed |= frame.result.visual_changed;
739 result.visual_changed |= frame.result.visual_changed;
740 result.structure_changed |= frame.result.structure_changed;
741 result.content_redrawn |= frame.rebuilt;
742 }
743 if result.content_redrawn {
744 self.prune_stale_draw_observations();
745 }
746 result.content_moved = std::mem::take(&mut self.app.content_moved);
747 result
748 }
749
750 fn prune_stale_draw_observations(&mut self) {
756 self.app.rebuilt_frames_since_observation_prune += 1;
757 if self.app.rebuilt_frames_since_observation_prune < OBSERVATION_PRUNE_INTERVAL {
758 return;
759 }
760 self.app.rebuilt_frames_since_observation_prune = 0;
761 let mut retained_visual_nodes = HashSet::default();
762 let mut any_graph = false;
763 for surface in &mut self.surfaces {
764 any_graph |= surface
765 .renderer
766 .scene()
767 .collect_retained_visual_observation_nodes(&mut surface.retained_visual_nodes);
768 retained_visual_nodes.extend(surface.retained_visual_nodes.iter().copied());
769 }
770 if any_graph {
771 cranpose_ui::prune_draw_observations_to_nodes(&retained_visual_nodes);
772 }
773 }
774}
775
776pub(crate) const OBSERVATION_PRUNE_INTERVAL: u32 = 60;
779
780fn render_surface<R>(
781 app: &mut ShellApp,
782 surface: &mut RootSurface<R>,
783 frame: &FrameDirt,
784) -> SurfaceFrame
785where
786 R: Renderer,
787 R::Error: Debug,
788{
789 let draw_dirty_nodes = std::mem::take(&mut surface.scoped_draw_nodes);
790 let layer_property_nodes = std::mem::take(&mut surface.scoped_layer_property_nodes);
791 let structural_parents = std::mem::take(&mut surface.structural_scene_nodes);
792 let draw_repass_pending = !draw_dirty_nodes.is_empty();
793 let mut draw_dirty_nodes = refresh_draw_nodes(app, surface, draw_dirty_nodes);
794 if frame.render_dirty && !frame.layer_properties_pending && !draw_repass_pending {
795 draw_dirty_nodes = refresh_retained_redraw_nodes(app, surface, draw_dirty_nodes);
796 }
797 let layout = cranpose_ui::GeometrySceneNodes {
798 reshaped: std::mem::take(&mut surface.scoped_layout_scene_nodes),
799 moved: std::mem::take(&mut surface.scoped_moved_scene_nodes),
800 };
801 let partial_dirty_nodes = std::mem::take(&mut surface.partial_scene_nodes);
802 let scene_dirty = surface.scene_dirty;
803 let mut dirt = SurfaceDirt::classify(
804 frame,
805 scene_dirty,
806 draw_repass_pending,
807 ScopedVisualNodes {
808 draw: draw_dirty_nodes,
809 layers: layer_property_nodes,
810 },
811 layout,
812 structural_parents,
813 partial_dirty_nodes,
814 );
815
816 if !dirt.needs_scene_rebuild {
817 dirt.log(
818 frame,
819 scene_dirty,
820 if dirt.render_only_dirty {
821 "present-retained"
822 } else {
823 "skip"
824 },
825 );
826 surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
827 surface.scoped_moved_scene_nodes = dirt.moved_nodes;
828 surface.scoped_draw_nodes = dirt.draw_dirty_nodes;
829 surface.scoped_layer_property_nodes = dirt.layer_property_nodes;
830 surface.structural_scene_nodes = dirt.structural_parents;
831 surface.partial_scene_nodes = dirt.partial_dirty_nodes;
832 if dirt.render_only_dirty && app.dev_options.fps_counter {
833 draw_dev_overlay(app, surface);
834 }
835 return SurfaceFrame {
836 result: FrameUpdateResult {
837 visual_changed: dirt.render_only_dirty,
838 ..FrameUpdateResult::default()
839 },
840 rebuilt: false,
841 };
842 }
843
844 surface.scene_dirty = false;
845 let structure_changed = !dirt.render_only_dirty && !dirt.use_visual_update();
846 rebuild_surface_scene(app, surface, frame, scene_dirty, &dirt);
847 if dev_overlay_belongs_on(surface.id, app.dev_options.fps_counter) {
848 draw_dev_overlay(app, surface);
849 }
850 dirt.layout_dirty_nodes.clear();
851 dirt.moved_nodes.clear();
852 surface.scoped_moved_scene_nodes = dirt.moved_nodes;
853 surface.scoped_draw_nodes = dirt.draw_dirty_nodes;
854 surface.scoped_layer_property_nodes = dirt.layer_property_nodes;
855 surface.structural_scene_nodes = dirt.structural_parents;
856 surface.partial_scene_nodes = dirt.partial_dirty_nodes;
857 surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
858 SurfaceFrame {
859 result: FrameUpdateResult {
860 visual_changed: true,
861 structure_changed,
862 ..FrameUpdateResult::default()
863 },
864 rebuilt: true,
865 }
866}
867
868fn rebuild_surface_scene<R>(
869 app: &mut ShellApp,
870 surface: &mut RootSurface<R>,
871 frame: &FrameDirt,
872 scene_dirty: bool,
873 dirt: &SurfaceDirt,
874) where
875 R: Renderer,
876 R::Error: Debug,
877{
878 let viewport_size = surface.viewport_size();
879 let Some(root) = surface.root_node(app) else {
880 surface.renderer.scene_mut().clear();
881 return;
882 };
883 let mut applier = app.composition.applier_mut();
884 dirt.log(frame, scene_dirty, dirt.rebuild_path());
885 let rebuild_result = if dirt.use_visual_update() {
886 surface.renderer.update_visual_scene_from_applier(
887 &mut applier,
888 root,
889 viewport_size,
890 SceneUpdates {
891 content: &dirt.partial_dirty_nodes,
892 layers: &dirt.layer_property_nodes,
893 moved: &dirt.moved_nodes,
894 },
895 )
896 } else if dirt.use_partial_update() {
897 surface.renderer.update_scene_from_applier(
898 &mut applier,
899 root,
900 viewport_size,
901 SceneUpdates {
902 content: &dirt.partial_dirty_nodes,
903 layers: &dirt.layer_property_nodes,
904 moved: &dirt.moved_nodes,
905 },
906 )
907 } else {
908 surface
909 .renderer
910 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
911 };
912 if let Err(err) = rebuild_result {
913 log::error!("renderer rebuild failed: {err:?}");
914 surface.renderer.scene_mut().clear();
915 }
916}
917
918fn dev_overlay_belongs_on(id: RootId, fps_counter: bool) -> bool {
919 fps_counter && matches!(id, RootId::Primary)
920}
921
922fn draw_dev_overlay<R: Renderer>(app: &ShellApp, surface: &mut RootSurface<R>) {
923 let viewport_size = surface.viewport_size();
924 surface.refresh_dev_overlay_text_for_frame_at(app, viewport_size, Instant::now());
925 surface
926 .renderer
927 .draw_dev_overlay(surface.dev_overlay_text.as_str(), viewport_size);
928}
929
930fn refresh_draw_nodes<R: Renderer>(
931 app: &mut ShellApp,
932 surface: &mut RootSurface<R>,
933 mut dirty_nodes: Vec<NodeId>,
934) -> Vec<NodeId> {
935 if dirty_nodes.is_empty() {
936 return dirty_nodes;
937 }
938 let Some(layout_tree) = surface.layout_tree.as_mut() else {
939 return dirty_nodes;
940 };
941
942 dirty_nodes.sort_unstable();
943 dirty_nodes.dedup();
944 let dirty_set: HashSet<NodeId> = dirty_nodes.iter().copied().collect();
945 let mut applier = app.composition.applier_mut();
946 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
947 refresh_layout_box_data(
948 &mut applier,
949 layout_tree.root_mut(),
950 &refresh_scope,
951 &dirty_set,
952 );
953 dirty_nodes
954}
955
956fn refresh_retained_redraw_nodes<R: Renderer>(
957 app: &mut ShellApp,
958 surface: &mut RootSurface<R>,
959 mut dirty_nodes: Vec<NodeId>,
960) -> Vec<NodeId> {
961 dirty_nodes.clear();
962 let Some(root) = surface.root_node(app) else {
963 return dirty_nodes;
964 };
965 {
966 let mut applier = app.composition.applier_mut();
967 collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
968 }
969 refresh_draw_nodes(app, surface, dirty_nodes)
970}
971
972impl<R: Renderer> RootSurface<R> {
973 fn refresh_dev_overlay_text_for_frame_at(
974 &mut self,
975 app: &ShellApp,
976 viewport_size: Size,
977 now: Instant,
978 ) {
979 if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
980 return;
981 }
982 self.dev_overlay_text = self.build_dev_overlay_text(app, viewport_size);
983 self.dev_overlay_last_refresh = Some(now);
984 self.dev_overlay_viewport = Some(viewport_size);
985 }
986
987 fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
988 if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
989 return true;
990 }
991
992 self.dev_overlay_last_refresh.is_none_or(|last| {
993 now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
994 })
995 }
996
997 fn build_dev_overlay_text(&mut self, app: &ShellApp, viewport_size: Size) -> String {
998 self.dev_overlay_controls.clear();
999
1000 let stats = app.fps_monitor.stats();
1001 let fields = [
1004 (0, format!("{:.0} FPS", stats.fps)),
1005 (3, format!("avg {:.1}ms", stats.avg_ms)),
1006 (2, format!("p95 {:.1}ms", stats.p95_ms)),
1007 (4, format!("max {:.1}ms", stats.max_ms)),
1008 (1, format!("work {:.1}ms", stats.work_avg_ms)),
1009 (5, format!("{} recomp/s", stats.recomps_per_second)),
1010 ];
1011 let line_len = |max_rank: u8| {
1012 let (count, chars) = fields
1013 .iter()
1014 .filter(|(rank, _)| *rank <= max_rank)
1015 .fold((0_usize, 0_usize), |(count, chars), (_, field)| {
1016 (count + 1, chars + field.len())
1017 });
1018 chars + count.saturating_sub(1) * DEV_OVERLAY_SEPARATOR.len()
1019 };
1020 let fitting_chars = ((viewport_size.width - DEV_OVERLAY_PADDING * 2.0)
1021 / DEV_OVERLAY_CHAR_WIDTH)
1022 .max(0.0) as usize;
1023 let controls_chars = if app.dev_options.frame_pacing_controls {
1024 DEV_OVERLAY_SEPARATOR.len() + frame_pacing_controls_len()
1025 } else {
1026 0
1027 };
1028 let mut max_rank = 5;
1029 while max_rank > 0 && line_len(max_rank) + controls_chars > fitting_chars {
1030 max_rank -= 1;
1031 }
1032 let mut text = String::with_capacity(line_len(max_rank) + controls_chars);
1033 for (_, field) in fields.iter().filter(|(rank, _)| *rank <= max_rank) {
1034 if !text.is_empty() {
1035 text.push_str(DEV_OVERLAY_SEPARATOR);
1036 }
1037 text.push_str(field);
1038 }
1039
1040 if !app.dev_options.frame_pacing_controls {
1041 return text;
1042 }
1043
1044 text.push_str(DEV_OVERLAY_SEPARATOR);
1045 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
1046 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
1047 if index > 0 {
1048 text.push(' ');
1049 }
1050 let start = text.len();
1051 if mode == app.dev_options.frame_pacing_mode {
1052 text.push('[');
1053 text.push_str(mode.label());
1054 text.push(']');
1055 } else {
1056 text.push_str(mode.label());
1057 }
1058 controls.push((start, text.len(), mode));
1059 }
1060
1061 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
1062 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
1063 .max(DEV_OVERLAY_PADDING);
1064 let overlay_y = DEV_OVERLAY_PADDING;
1065 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
1066 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
1067 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
1068
1069 self.dev_overlay_controls = controls
1070 .into_iter()
1071 .map(|(start, end, mode)| DevOverlayControl {
1072 bounds: Rect {
1073 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
1074 y: text_y - 3.0,
1075 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
1076 height: text_height + 6.0,
1077 },
1078 mode,
1079 })
1080 .collect();
1081
1082 text
1083 }
1084}
1085
1086fn frame_pacing_controls_len() -> usize {
1089 let labels: usize = FramePacingMode::ALL
1090 .iter()
1091 .map(|mode| mode.label().len())
1092 .sum();
1093 labels + FramePacingMode::ALL.len() - 1 + 2
1094}
1095
1096fn clear_dispatch_invalidation(
1097 applier: &mut MemoryApplier,
1098 node_id: NodeId,
1099 invalidation: DispatchInvalidationKind,
1100) -> Result<bool, NodeError> {
1101 match invalidation {
1102 DispatchInvalidationKind::Pointer => {
1103 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1104 let needs_pointer_pass = node.needs_pointer_pass();
1105 if needs_pointer_pass {
1106 node.clear_needs_pointer_pass();
1107 }
1108 needs_pointer_pass
1109 }) {
1110 Ok(cleared) => Ok(cleared),
1111 Err(NodeError::TypeMismatch { .. }) => applier
1112 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1113 let needs_pointer_pass = node.needs_pointer_pass();
1114 if needs_pointer_pass {
1115 node.clear_needs_pointer_pass();
1116 }
1117 needs_pointer_pass
1118 }),
1119 Err(err) => Err(err),
1120 }
1121 }
1122 DispatchInvalidationKind::Focus => {
1123 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1124 let needs_focus_sync = node.needs_focus_sync();
1125 if needs_focus_sync {
1126 node.clear_needs_focus_sync();
1127 }
1128 needs_focus_sync
1129 }) {
1130 Ok(cleared) => Ok(cleared),
1131 Err(NodeError::TypeMismatch { .. }) => applier
1132 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1133 let needs_focus_sync = node.needs_focus_sync();
1134 if needs_focus_sync {
1135 node.clear_needs_focus_sync();
1136 }
1137 needs_focus_sync
1138 }),
1139 Err(err) => Err(err),
1140 }
1141 }
1142 }
1143}
1144
1145pub(crate) fn build_draw_refresh_scope(
1146 applier: &mut MemoryApplier,
1147 dirty_nodes: &HashSet<NodeId>,
1148) -> HashSet<NodeId> {
1149 let mut refresh_scope =
1150 HashSet::with_capacity_and_hasher(dirty_nodes.len(), Default::default());
1151 for &dirty_node in dirty_nodes {
1152 let mut current = Some(dirty_node);
1153 while let Some(node_id) = current {
1154 if !refresh_scope.insert(node_id) {
1155 break;
1156 }
1157 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
1158 }
1159 }
1160 refresh_scope
1161}
1162
1163fn take_needs_redraw(applier: &mut MemoryApplier, node_id: NodeId) -> bool {
1164 match applier.with_node::<LayoutNode, _>(node_id, |node| {
1165 let needs_redraw = node.needs_redraw();
1166 if needs_redraw {
1167 node.clear_needs_redraw();
1168 }
1169 needs_redraw
1170 }) {
1171 Ok(needs_redraw) => needs_redraw,
1172 Err(NodeError::TypeMismatch { .. }) => applier
1173 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1174 let needs_redraw = node.needs_redraw();
1175 if needs_redraw {
1176 node.clear_needs_redraw();
1177 }
1178 needs_redraw
1179 })
1180 .unwrap_or(false),
1181 Err(_) => false,
1182 }
1183}
1184
1185fn collect_retained_redraw_nodes(
1186 applier: &mut MemoryApplier,
1187 root: NodeId,
1188 dirty_nodes: &mut Vec<NodeId>,
1189) {
1190 let mut children = Default::default();
1191 match applier.get_mut(root) {
1192 Ok(node) => node.collect_children_into(&mut children),
1193 Err(_) => return,
1194 }
1195
1196 if take_needs_redraw(applier, root) {
1197 dirty_nodes.push(root);
1198 }
1199
1200 for child in children {
1201 if cranpose_ui::is_window_root(applier, child) {
1202 continue;
1203 }
1204 collect_retained_redraw_nodes(applier, child, dirty_nodes);
1205 }
1206}
1207
1208fn refresh_layout_box_data(
1209 applier: &mut MemoryApplier,
1210 layout: &mut cranpose_ui::layout::LayoutBox,
1211 refresh_scope: &HashSet<NodeId>,
1212 dirty_nodes: &HashSet<NodeId>,
1213) {
1214 if !refresh_scope.contains(&layout.node_id) {
1215 return;
1216 }
1217
1218 if dirty_nodes.contains(&layout.node_id) {
1219 let refreshed = applier
1220 .with_node::<LayoutNode, _>(layout.node_id, |node| {
1221 node.clear_needs_redraw();
1222 (
1223 node.modifier.clone(),
1224 node.resolved_modifiers(),
1225 node.modifier_slices_snapshot(),
1226 node.semantics_configuration(),
1227 )
1228 })
1229 .or_else(|_| {
1230 applier.with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1231 node.clear_needs_redraw();
1232 (
1233 node.modifier(),
1234 node.resolved_modifiers(),
1235 node.modifier_slices_snapshot(),
1236 node.semantics_configuration(),
1237 )
1238 })
1239 });
1240 if let Ok((modifier, resolved_modifiers, slices, semantics)) = refreshed {
1241 layout.node_data.modifier = modifier;
1242 layout.node_data.resolved_modifiers = resolved_modifiers;
1243 layout.node_data.modifier_slices = slices;
1244 layout.node_data.semantics = semantics.map(std::rc::Rc::new);
1245 }
1246 }
1247
1248 for child in &mut layout.children {
1249 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1250 }
1251}
1252
1253#[cfg(test)]
1254#[path = "tests/shell_frame_tests.rs"]
1255mod tests;