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