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