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