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
265impl<R> AppShell<R>
266where
267 R: Renderer,
268 R::Error: Debug,
269{
270 pub fn set_semantics_enabled(&mut self, enabled: bool) {
271 if self.app.semantics_enabled == enabled {
272 return;
273 }
274 self.app.semantics_enabled = enabled;
275 self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
276 if enabled {
277 self.app.request_forced_layout_pass();
278 self.mark_all_dirty();
279 } else {
280 for surface in &mut self.surfaces {
281 surface.semantics_tree = None;
282 }
283 }
284 }
285
286 pub fn set_frame_rate_preference(&mut self, preference: crate::FrameRatePreference) {
291 self.surfaces[0].frame_rate_preference = preference;
292 }
293
294 pub fn frame_rate_preference(&self) -> crate::FrameRatePreference {
298 self.surfaces[0].frame_rate_preference
299 }
300
301 pub(crate) fn process_frame(&mut self) -> FrameUpdateResult {
302 let app_context = Rc::clone(&self.app.app_context);
303 app_context.enter(|| self.process_frame_in_context(false))
304 }
305
306 pub(crate) fn process_frame_in_context(
307 &mut self,
308 recomposed_before_frame: bool,
309 ) -> FrameUpdateResult {
310 let frame_start = Instant::now();
311
312 self.sync_window_roots_in_context();
313 self.run_layout_phase_in_context();
314 let after_initial_layout = Instant::now();
315
316 let recomposed_this_frame = recomposed_before_frame || self.run_post_layout_recomposition();
317
318 let after_layout = Instant::now();
319
320 self.run_dispatch_queues();
321
322 let after_dispatch = Instant::now();
323
324 clear_transient_scroll_motion_contexts();
325
326 let result = self.run_render_phase_in_context(recomposed_this_frame);
327 let after_render = Instant::now();
328 log_frame_stage_telemetry(
329 frame_start,
330 after_initial_layout,
331 after_layout,
332 after_dispatch,
333 after_render,
334 );
335 result
336 }
337
338 #[cfg(test)]
339 pub(crate) fn run_layout_phase(&mut self) {
340 let app_context = Rc::clone(&self.app.app_context);
341 app_context.enter(|| self.run_layout_phase_in_context());
342 }
343
344 fn run_layout_phase_in_context(&mut self) {
345 let has_scoped_repasses = cranpose_ui::has_pending_layout_repasses()
346 || cranpose_ui::has_pending_measure_repasses();
347 let scoped_layout_nodes = pending_repass_nodes();
348
349 let invalidation_requested = take_layout_invalidation();
350 let global_layout_invalidation = invalidation_requested && !has_scoped_repasses;
351 let force_layout_pass = self.app.force_layout_pass;
352
353 if global_layout_invalidation {
354 cranpose_ui::layout::invalidate_all_layout_caches();
355 if let Some(root) = self.app.composition.root() {
356 mark_root_for_layout(&mut self.app.composition.applier_mut(), root);
357 }
358 self.app.request_forced_layout_pass();
359 } else if invalidation_requested || has_scoped_repasses {
360 self.app.request_layout_pass();
361 }
362
363 if !self.app.layout_requested {
364 return;
365 }
366
367 let Some(root) = self.app.composition.root() else {
368 self.reset_layout_snapshots();
369 return;
370 };
371 let viewport_size = self.surfaces[0].viewport_size();
372 let handle = self.app.composition.runtime_handle();
373 let mut applier = self.app.composition.applier_mut();
374 applier.set_runtime_handle(handle);
375
376 let tree_needs_layout_check = cranpose_ui::tree_needs_layout(&mut *applier, root)
377 .unwrap_or_else(|err| {
378 log::warn!(
379 "Cannot check layout dirty status for root #{}: {}",
380 root,
381 err
382 );
383 true
384 });
385 let needs_layout =
386 self.app.force_layout_pass || has_scoped_repasses || tree_needs_layout_check;
387 self.app.layout_requested = false;
388 self.app.force_layout_pass = false;
389
390 if !needs_layout {
391 log::trace!("Skipping layout: tree is clean");
392 applier.clear_runtime_handle();
393 return;
394 }
395
396 let measured = cranpose_ui::measure_layout_with_options(
397 &mut applier,
398 root,
399 viewport_size,
400 MeasureLayoutOptions {
401 collect_semantics: false,
402 build_layout_tree: false,
403 },
404 );
405 applier.clear_runtime_handle();
406 drop(applier);
407 match measured {
408 Ok(_measurements) => {
409 self.forget_frame_snapshots();
410 self.record_layout_scene_nodes(
411 global_layout_invalidation || force_layout_pass,
412 has_scoped_repasses,
413 scoped_layout_nodes,
414 );
415 }
416 Err(err) => {
417 log::error!("failed to compute layout: {err}");
418 self.reset_layout_snapshots();
419 }
420 }
421 }
422
423 fn forget_frame_snapshots(&mut self) {
424 self.app.semantics_snapshot_revision = self.app.semantics_snapshot_revision.wrapping_add(1);
425 for surface in &mut self.surfaces {
426 surface.forget_snapshots();
427 }
428 }
429
430 fn reset_layout_snapshots(&mut self) {
431 self.forget_frame_snapshots();
432 for surface in &mut self.surfaces {
433 surface.scoped_layout_scene_nodes.clear();
434 surface.scene_dirty = true;
435 }
436 let _ = cranpose_ui::take_geometry_scene_nodes();
437 self.app.layout_requested = false;
438 self.app.force_layout_pass = false;
439 }
440
441 fn record_layout_scene_nodes(
442 &mut self,
443 global: bool,
444 has_scoped_repasses: bool,
445 scoped_layout_nodes: Vec<NodeId>,
446 ) {
447 if global {
448 for surface in &mut self.surfaces {
449 surface.scoped_layout_scene_nodes.clear();
450 surface.scene_dirty = true;
451 }
452 let _ = cranpose_ui::take_geometry_scene_nodes();
453 return;
454 }
455 let mut nodes = if has_scoped_repasses {
456 scoped_layout_nodes
457 } else {
458 Vec::new()
459 };
460 if let Some(root) = self.app.composition.root() {
461 let mut applier = self.app.composition.applier_mut();
462 for node in cranpose_ui::take_geometry_scene_nodes() {
463 if let Some(node) = applier.scene_node_attached_to(node, root) {
464 nodes.push(node);
465 }
466 }
467 }
468 let buckets = partition_nodes_by_surface(&mut self.app, &self.surfaces, nodes);
469 let mut any_named = false;
470 for (surface, bucket) in self.surfaces.iter_mut().zip(buckets) {
471 if bucket.is_empty() {
472 continue;
473 }
474 any_named = true;
475 surface.scene_dirty = true;
476 let mut seen: HashSet<NodeId> =
477 surface.scoped_layout_scene_nodes.iter().copied().collect();
478 for node in bucket {
479 if seen.insert(node) {
480 surface.scoped_layout_scene_nodes.push(node);
481 }
482 }
483 }
484 if !any_named {
485 for surface in &mut self.surfaces {
486 surface.scene_dirty = true;
487 }
488 }
489 }
490
491 fn run_post_layout_recomposition(&mut self) -> bool {
492 if !self.app.composition.should_recompose() {
493 return false;
494 }
495
496 let Some(root_key) = self.app.composition.root_key() else {
497 return false;
498 };
499
500 match self
501 .app
502 .composition
503 .reconcile(root_key, &mut *self.app.content)
504 {
505 Ok(changed) => {
506 if !changed {
507 return false;
508 }
509 self.app.fps_monitor.record_recomposition();
510 self.sync_window_roots_in_context();
511 if self.app.composition_tree_needs_layout() {
512 self.app.request_layout_pass();
513 self.run_layout_phase_in_context();
514 }
515 request_render_invalidation();
516 true
517 }
518 Err(NodeError::Missing { id }) => {
519 log::debug!(
520 "Post-layout recomposition skipped: node {} no longer exists",
521 id
522 );
523 self.app.request_layout_pass();
524 request_render_invalidation();
525 true
526 }
527 Err(err) => {
528 log::error!("post-layout recomposition failed: {err}");
529 self.app.request_layout_pass();
530 request_render_invalidation();
531 true
532 }
533 }
534 }
535
536 fn run_dispatch_queues(&mut self) {
537 if has_pending_pointer_repasses() {
538 let mut applier = self.app.composition.applier_mut();
539 process_pointer_repasses(|node_id| {
540 match clear_dispatch_invalidation(
541 &mut applier,
542 node_id,
543 DispatchInvalidationKind::Pointer,
544 ) {
545 Ok(true) => {
546 log::trace!("Cleared pointer repass flag for node #{}", node_id);
547 }
548 Ok(false) => {}
549 Err(err) => {
550 log::debug!(
551 "Could not process pointer repass for node #{}: {}",
552 node_id,
553 err
554 );
555 }
556 }
557 });
558 }
559
560 if has_pending_focus_invalidations() {
561 let mut applier = self.app.composition.applier_mut();
562 process_focus_invalidations(|node_id| {
563 match clear_dispatch_invalidation(
564 &mut applier,
565 node_id,
566 DispatchInvalidationKind::Focus,
567 ) {
568 Ok(true) => {
569 log::trace!("Cleared focus sync flag for node #{}", node_id);
570 }
571 Ok(false) => {}
572 Err(err) => {
573 log::debug!(
574 "Could not process focus invalidation for node #{}: {}",
575 node_id,
576 err
577 );
578 }
579 }
580 });
581 }
582
583 if has_pending_semantics_invalidations() {
584 let mut applier = self.app.composition.applier_mut();
585 process_semantics_invalidations(|node_id| {
586 cranpose_core::bubble_semantics_dirty(&mut *applier, node_id);
587 });
588 }
589 }
590
591 fn take_structural_change_parents(&mut self) -> Vec<NodeId> {
592 if cranpose_core::env_flag!("CRANPOSE_DISABLE_STRUCTURAL_DIRT") {
593 return Vec::new();
594 }
595 let Some(root) = self.app.composition.root() else {
596 return Vec::new();
597 };
598 self.app
599 .composition
600 .applier_mut()
601 .take_structural_change_parents_attached_to(root)
602 }
603
604 #[cfg(test)]
605 pub(crate) fn run_render_phase(&mut self) -> FrameUpdateResult {
606 let app_context = Rc::clone(&self.app.app_context);
607 app_context.enter(|| self.run_render_phase_in_context(false))
608 }
609
610 fn run_render_phase_in_context(&mut self, recomposed_this_frame: bool) -> FrameUpdateResult {
611 cranpose_ui::tick_cursor_blink();
612 let render_dirty = take_render_invalidation();
613 let pointer_dirty = take_pointer_invalidation();
614 take_focus_invalidation();
615 let draw_dirty =
616 partition_nodes_by_surface(&mut self.app, &self.surfaces, take_draw_repass_nodes());
617 let structural = self.take_structural_change_parents();
618 let structural = partition_nodes_by_surface(&mut self.app, &self.surfaces, structural);
619 let _ = cranpose_ui::has_focused_field();
620 let attributed = draw_dirty
621 .iter()
622 .chain(structural.iter())
623 .any(|nodes| !nodes.is_empty())
624 || self
625 .surfaces
626 .iter()
627 .any(|surface| !surface.scoped_layout_scene_nodes.is_empty());
628 let frame = FrameDirt {
629 render_dirty,
630 pointer_dirty,
631 attributed,
632 recomposed_this_frame,
633 };
634
635 let mut result = FrameUpdateResult::default();
636 let mut retained_visual_nodes = HashSet::new();
637 let mut prune_observations = false;
638 for ((surface, draw_dirty), structural) in
639 self.surfaces.iter_mut().zip(draw_dirty).zip(structural)
640 {
641 let frame = render_surface(&mut self.app, surface, &frame, draw_dirty, structural);
642 surface.last_update = frame.result;
643 surface.frame_owed |= frame.result.visual_changed;
644 result.visual_changed |= frame.result.visual_changed;
645 result.structure_changed |= frame.result.structure_changed;
646 if frame.rebuilt
647 && surface
648 .renderer
649 .scene()
650 .collect_retained_visual_observation_nodes(&mut surface.retained_visual_nodes)
651 {
652 prune_observations = true;
653 }
654 retained_visual_nodes.extend(surface.retained_visual_nodes.iter().copied());
655 }
656 if prune_observations {
657 cranpose_ui::prune_draw_observations_to_nodes(&retained_visual_nodes);
658 }
659 result
660 }
661}
662
663fn render_surface<R>(
664 app: &mut ShellApp,
665 surface: &mut RootSurface<R>,
666 frame: &FrameDirt,
667 draw_dirty_nodes: Vec<NodeId>,
668 structural_parents: Vec<NodeId>,
669) -> SurfaceFrame
670where
671 R: Renderer,
672 R::Error: Debug,
673{
674 let draw_repass_pending = !draw_dirty_nodes.is_empty();
675 let mut draw_dirty_nodes = refresh_draw_nodes(app, surface, draw_dirty_nodes);
676 if frame.render_dirty && !draw_repass_pending {
677 draw_dirty_nodes = refresh_retained_redraw_nodes(app, surface);
678 }
679 let layout_dirty_nodes = std::mem::take(&mut surface.scoped_layout_scene_nodes);
680 let scene_dirty = surface.scene_dirty;
681 let dirt = SurfaceDirt::classify(
682 frame,
683 scene_dirty,
684 draw_repass_pending,
685 draw_dirty_nodes,
686 layout_dirty_nodes,
687 structural_parents,
688 );
689
690 if !dirt.needs_scene_rebuild {
691 dirt.log(
692 frame,
693 scene_dirty,
694 if dirt.render_only_dirty {
695 "present-retained"
696 } else {
697 "skip"
698 },
699 );
700 surface.scoped_layout_scene_nodes = dirt.layout_dirty_nodes;
701 if dirt.render_only_dirty && app.dev_options.fps_counter {
702 draw_dev_overlay(app, surface);
703 }
704 return SurfaceFrame {
705 result: FrameUpdateResult {
706 visual_changed: dirt.render_only_dirty,
707 structure_changed: false,
708 },
709 rebuilt: false,
710 };
711 }
712
713 surface.scene_dirty = false;
714 let structure_changed = !dirt.render_only_dirty && !dirt.visual_update_only();
715 rebuild_surface_scene(app, surface, frame, scene_dirty, &dirt);
716 if dev_overlay_belongs_on(surface.id, app.dev_options.fps_counter) {
717 draw_dev_overlay(app, surface);
718 }
719 SurfaceFrame {
720 result: FrameUpdateResult {
721 visual_changed: true,
722 structure_changed,
723 },
724 rebuilt: true,
725 }
726}
727
728fn rebuild_surface_scene<R>(
729 app: &mut ShellApp,
730 surface: &mut RootSurface<R>,
731 frame: &FrameDirt,
732 scene_dirty: bool,
733 dirt: &SurfaceDirt,
734) where
735 R: Renderer,
736 R::Error: Debug,
737{
738 let viewport_size = surface.viewport_size();
739 let Some(root) = surface.root_node(app) else {
740 surface.renderer.scene_mut().clear();
741 return;
742 };
743 let mut applier = app.composition.applier_mut();
744 dirt.log(frame, scene_dirty, dirt.rebuild_path());
745 let rebuild_result = if dirt.use_visual_update() {
746 surface.renderer.update_visual_scene_from_applier(
747 &mut applier,
748 root,
749 viewport_size,
750 &dirt.partial_dirty_nodes,
751 )
752 } else if dirt.use_partial_update() {
753 surface.renderer.update_scene_from_applier(
754 &mut applier,
755 root,
756 viewport_size,
757 &dirt.partial_dirty_nodes,
758 )
759 } else {
760 surface
761 .renderer
762 .rebuild_scene_from_applier(&mut applier, root, viewport_size)
763 };
764 if let Err(err) = rebuild_result {
765 log::error!("renderer rebuild failed: {err:?}");
766 surface.renderer.scene_mut().clear();
767 }
768}
769
770fn dev_overlay_belongs_on(id: RootId, fps_counter: bool) -> bool {
771 fps_counter && matches!(id, RootId::Primary)
772}
773
774fn draw_dev_overlay<R: Renderer>(app: &ShellApp, surface: &mut RootSurface<R>) {
775 let viewport_size = surface.viewport_size();
776 surface.refresh_dev_overlay_text_for_frame_at(app, viewport_size, Instant::now());
777 surface
778 .renderer
779 .draw_dev_overlay(surface.dev_overlay_text.as_str(), viewport_size);
780}
781
782fn refresh_draw_nodes<R: Renderer>(
783 app: &mut ShellApp,
784 surface: &mut RootSurface<R>,
785 dirty_nodes: Vec<NodeId>,
786) -> Vec<NodeId> {
787 if dirty_nodes.is_empty() {
788 return dirty_nodes;
789 }
790 let Some(layout_tree) = surface.layout_tree.as_mut() else {
791 return dirty_nodes;
792 };
793
794 let dirty_set: HashSet<NodeId> = dirty_nodes.into_iter().collect();
795 let mut applier = app.composition.applier_mut();
796 let refresh_scope = build_draw_refresh_scope(&mut applier, &dirty_set);
797 refresh_layout_box_data(
798 &mut applier,
799 layout_tree.root_mut(),
800 &refresh_scope,
801 &dirty_set,
802 );
803 dirty_set.into_iter().collect()
804}
805
806fn refresh_retained_redraw_nodes<R: Renderer>(
807 app: &mut ShellApp,
808 surface: &mut RootSurface<R>,
809) -> Vec<NodeId> {
810 let Some(root) = surface.root_node(app) else {
811 return Vec::new();
812 };
813 let mut dirty_nodes = Vec::new();
814 {
815 let mut applier = app.composition.applier_mut();
816 collect_retained_redraw_nodes(&mut applier, root, &mut dirty_nodes);
817 }
818 refresh_draw_nodes(app, surface, dirty_nodes)
819}
820
821impl<R: Renderer> RootSurface<R> {
822 fn refresh_dev_overlay_text_for_frame_at(
823 &mut self,
824 app: &ShellApp,
825 viewport_size: Size,
826 now: Instant,
827 ) {
828 if !self.dev_overlay_text_needs_refresh(viewport_size, now) {
829 return;
830 }
831 self.dev_overlay_text = self.build_dev_overlay_text(app, viewport_size);
832 self.dev_overlay_last_refresh = Some(now);
833 self.dev_overlay_viewport = Some(viewport_size);
834 }
835
836 fn dev_overlay_text_needs_refresh(&self, viewport_size: Size, now: Instant) -> bool {
837 if self.dev_overlay_text.is_empty() || self.dev_overlay_viewport != Some(viewport_size) {
838 return true;
839 }
840
841 self.dev_overlay_last_refresh
842 .map(|last| {
843 now.checked_duration_since(last).unwrap_or_default() >= DEV_OVERLAY_REFRESH_INTERVAL
844 })
845 .unwrap_or(true)
846 }
847
848 fn build_dev_overlay_text(&mut self, app: &ShellApp, viewport_size: Size) -> String {
849 self.dev_overlay_controls.clear();
850
851 let stats = app.fps_monitor.stats();
852 let mut text = format!(
853 "{:.0} FPS | avg {:.1}ms | p95 {:.1}ms | max {:.1}ms | work {:.1}ms | {} recomp/s",
854 stats.fps,
855 stats.avg_ms,
856 stats.p95_ms,
857 stats.max_ms,
858 stats.work_avg_ms,
859 stats.recomps_per_second
860 );
861
862 if !app.dev_options.frame_pacing_controls {
863 return text;
864 }
865
866 text.push_str(" | ");
867 let mut controls = Vec::with_capacity(FramePacingMode::ALL.len());
868 for (index, mode) in FramePacingMode::ALL.into_iter().enumerate() {
869 if index > 0 {
870 text.push(' ');
871 }
872 let start = text.len();
873 if mode == app.dev_options.frame_pacing_mode {
874 text.push('[');
875 text.push_str(mode.label());
876 text.push(']');
877 } else {
878 text.push_str(mode.label());
879 }
880 controls.push((start, text.len(), mode));
881 }
882
883 let overlay_width = text.len() as f32 * DEV_OVERLAY_CHAR_WIDTH;
884 let overlay_x = (viewport_size.width - overlay_width - DEV_OVERLAY_PADDING * 2.0)
885 .max(DEV_OVERLAY_PADDING);
886 let overlay_y = DEV_OVERLAY_PADDING;
887 let text_x = overlay_x + DEV_OVERLAY_PADDING / 2.0;
888 let text_y = overlay_y + DEV_OVERLAY_PADDING / 4.0;
889 let text_height = DEV_OVERLAY_FONT_SIZE * 1.4;
890
891 self.dev_overlay_controls = controls
892 .into_iter()
893 .map(|(start, end, mode)| DevOverlayControl {
894 bounds: Rect {
895 x: text_x + start as f32 * DEV_OVERLAY_CHAR_WIDTH - 3.0,
896 y: text_y - 3.0,
897 width: (end - start) as f32 * DEV_OVERLAY_CHAR_WIDTH + 6.0,
898 height: text_height + 6.0,
899 },
900 mode,
901 })
902 .collect();
903
904 text
905 }
906}
907
908fn clear_dispatch_invalidation(
909 applier: &mut MemoryApplier,
910 node_id: NodeId,
911 invalidation: DispatchInvalidationKind,
912) -> Result<bool, NodeError> {
913 match invalidation {
914 DispatchInvalidationKind::Pointer => {
915 match applier.with_node::<LayoutNode, _>(node_id, |node| {
916 let needs_pointer_pass = node.needs_pointer_pass();
917 if needs_pointer_pass {
918 node.clear_needs_pointer_pass();
919 }
920 needs_pointer_pass
921 }) {
922 Ok(cleared) => Ok(cleared),
923 Err(NodeError::TypeMismatch { .. }) => applier
924 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
925 let needs_pointer_pass = node.needs_pointer_pass();
926 if needs_pointer_pass {
927 node.clear_needs_pointer_pass();
928 }
929 needs_pointer_pass
930 }),
931 Err(err) => Err(err),
932 }
933 }
934 DispatchInvalidationKind::Focus => {
935 match applier.with_node::<LayoutNode, _>(node_id, |node| {
936 let needs_focus_sync = node.needs_focus_sync();
937 if needs_focus_sync {
938 node.clear_needs_focus_sync();
939 }
940 needs_focus_sync
941 }) {
942 Ok(cleared) => Ok(cleared),
943 Err(NodeError::TypeMismatch { .. }) => applier
944 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
945 let needs_focus_sync = node.needs_focus_sync();
946 if needs_focus_sync {
947 node.clear_needs_focus_sync();
948 }
949 needs_focus_sync
950 }),
951 Err(err) => Err(err),
952 }
953 }
954 }
955}
956
957pub(crate) fn build_draw_refresh_scope(
958 applier: &mut MemoryApplier,
959 dirty_nodes: &HashSet<NodeId>,
960) -> HashSet<NodeId> {
961 let mut refresh_scope = HashSet::with_capacity(dirty_nodes.len());
962 for &dirty_node in dirty_nodes {
963 let mut current = Some(dirty_node);
964 while let Some(node_id) = current {
965 if !refresh_scope.insert(node_id) {
966 break;
967 }
968 current = applier.get_mut(node_id).ok().and_then(|node| node.parent());
969 }
970 }
971 refresh_scope
972}
973
974fn take_needs_redraw(applier: &mut MemoryApplier, node_id: NodeId) -> bool {
975 match applier.with_node::<LayoutNode, _>(node_id, |node| {
976 let needs_redraw = node.needs_redraw();
977 if needs_redraw {
978 node.clear_needs_redraw();
979 }
980 needs_redraw
981 }) {
982 Ok(needs_redraw) => needs_redraw,
983 Err(NodeError::TypeMismatch { .. }) => applier
984 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
985 let needs_redraw = node.needs_redraw();
986 if needs_redraw {
987 node.clear_needs_redraw();
988 }
989 needs_redraw
990 })
991 .unwrap_or(false),
992 Err(_) => false,
993 }
994}
995
996fn collect_retained_redraw_nodes(
997 applier: &mut MemoryApplier,
998 root: NodeId,
999 dirty_nodes: &mut Vec<NodeId>,
1000) {
1001 let mut children = Default::default();
1002 match applier.get_mut(root) {
1003 Ok(node) => node.collect_children_into(&mut children),
1004 Err(_) => return,
1005 }
1006
1007 if take_needs_redraw(applier, root) {
1008 dirty_nodes.push(root);
1009 }
1010
1011 for child in children {
1012 if cranpose_ui::is_window_root(applier, child) {
1013 continue;
1014 }
1015 collect_retained_redraw_nodes(applier, child, dirty_nodes);
1016 }
1017}
1018
1019fn refresh_layout_box_data(
1020 applier: &mut MemoryApplier,
1021 layout: &mut cranpose_ui::layout::LayoutBox,
1022 refresh_scope: &HashSet<NodeId>,
1023 dirty_nodes: &HashSet<NodeId>,
1024) {
1025 if !refresh_scope.contains(&layout.node_id) {
1026 return;
1027 }
1028
1029 if dirty_nodes.contains(&layout.node_id) {
1030 if let Ok((modifier, resolved_modifiers, slices)) =
1031 applier.with_node::<LayoutNode, _>(layout.node_id, |node| {
1032 node.clear_needs_redraw();
1033 (
1034 node.modifier.clone(),
1035 node.resolved_modifiers(),
1036 node.modifier_slices_snapshot(),
1037 )
1038 })
1039 {
1040 layout.node_data.modifier = modifier;
1041 layout.node_data.resolved_modifiers = resolved_modifiers;
1042 layout.node_data.modifier_slices = slices;
1043 } else if let Ok((modifier, resolved_modifiers)) = applier
1044 .with_node::<SubcomposeLayoutNode, _>(layout.node_id, |node| {
1045 node.clear_needs_redraw();
1046 (node.modifier(), node.resolved_modifiers())
1047 })
1048 {
1049 layout.node_data.modifier = modifier.clone();
1050 layout.node_data.resolved_modifiers = resolved_modifiers;
1051 layout.node_data.modifier_slices =
1052 std::rc::Rc::new(cranpose_ui::collect_slices_from_modifier(&modifier));
1053 }
1054 }
1055
1056 for child in &mut layout.children {
1057 refresh_layout_box_data(applier, child, refresh_scope, dirty_nodes);
1058 }
1059}
1060
1061#[cfg(test)]
1062#[path = "tests/shell_frame_tests.rs"]
1063mod tests;