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