1use crate::surface_requirements::{SurfaceRequirement, SurfaceRequirementSet};
7use std::cell::{Cell, RefCell};
8
9use cranpose_core::NodeId;
10use cranpose_ui_graphics::Rect;
11
12const TOP_ISOLATED_LAYER_LIMIT: usize = 8;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct LayerSurfaceReasons {
16 pub explicit_offscreen: bool,
17 pub effect: bool,
18 pub backdrop: bool,
19 pub group_opacity: bool,
20 pub blend_mode: bool,
21 pub immediate_shadow: bool,
22 pub text_local_surface: bool,
23 pub motion_stable_capture: bool,
24 pub mixed_direct_content: bool,
25 pub non_translation_transform: bool,
26}
27
28impl LayerSurfaceReasons {
29 pub fn has_any(self) -> bool {
34 self.explicit_offscreen
35 || self.effect
36 || self.backdrop
37 || self.group_opacity
38 || self.blend_mode
39 || self.immediate_shadow
40 || self.text_local_surface
41 || self.motion_stable_capture
42 || self.non_translation_transform
43 }
44
45 pub fn labels(self) -> impl Iterator<Item = &'static str> {
46 let mut labels = [None; 10];
47 let mut len = 0usize;
48
49 if self.explicit_offscreen {
50 labels[len] = Some("explicit_offscreen");
51 len += 1;
52 }
53 if self.effect {
54 labels[len] = Some("effect");
55 len += 1;
56 }
57 if self.backdrop {
58 labels[len] = Some("backdrop");
59 len += 1;
60 }
61 if self.group_opacity {
62 labels[len] = Some("group_opacity");
63 len += 1;
64 }
65 if self.blend_mode {
66 labels[len] = Some("blend_mode");
67 len += 1;
68 }
69 if self.immediate_shadow {
70 labels[len] = Some("immediate_shadow");
71 len += 1;
72 }
73 if self.text_local_surface {
74 labels[len] = Some("text_local_surface");
75 len += 1;
76 }
77 if self.motion_stable_capture {
78 labels[len] = Some("motion_stable_capture");
79 len += 1;
80 }
81 if self.mixed_direct_content {
82 labels[len] = Some("mixed_direct_content");
83 len += 1;
84 }
85 if self.non_translation_transform {
86 labels[len] = Some("non_translation_transform");
87 len += 1;
88 }
89
90 labels.into_iter().flatten().take(len)
91 }
92
93 pub fn display(self) -> String {
94 let mut joined = String::new();
95 for (index, label) in self.labels().enumerate() {
96 if index > 0 {
97 joined.push('+');
98 }
99 joined.push_str(label);
100 }
101 if joined.is_empty() {
102 joined.push_str("none");
103 }
104 joined
105 }
106
107 pub fn has_renderer_forced_surface(self) -> bool {
108 self.immediate_shadow || self.text_local_surface || self.non_translation_transform
109 }
110}
111
112impl From<SurfaceRequirementSet> for LayerSurfaceReasons {
113 fn from(requirements: SurfaceRequirementSet) -> Self {
114 Self {
115 explicit_offscreen: requirements.contains(SurfaceRequirement::ExplicitOffscreen),
116 effect: requirements.contains(SurfaceRequirement::RenderEffect),
117 backdrop: requirements.contains(SurfaceRequirement::Backdrop),
118 group_opacity: requirements.contains(SurfaceRequirement::GroupOpacity),
119 blend_mode: requirements.contains(SurfaceRequirement::BlendMode),
120 immediate_shadow: requirements.contains(SurfaceRequirement::ImmediateShadow),
121 text_local_surface: requirements.contains(SurfaceRequirement::TextMaterialMask),
122 motion_stable_capture: requirements.contains(SurfaceRequirement::MotionStableCapture),
123 mixed_direct_content: requirements.contains(SurfaceRequirement::MixedDirectContent),
124 non_translation_transform: requirements
125 .contains(SurfaceRequirement::NonTranslationTransform),
126 }
127 }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq)]
131pub struct IsolatedLayerStat {
132 pub node_id: Option<NodeId>,
133 pub logical_rect: Rect,
134 pub width: u32,
135 pub height: u32,
136 pub reasons: LayerSurfaceReasons,
137}
138
139impl IsolatedLayerStat {
140 fn pixel_area(self) -> u64 {
141 (self.width as u64) * (self.height as u64)
142 }
143}
144
145impl Default for IsolatedLayerStat {
146 fn default() -> Self {
147 Self {
148 node_id: None,
149 logical_rect: Rect {
150 x: 0.0,
151 y: 0.0,
152 width: 0.0,
153 height: 0.0,
154 },
155 width: 0,
156 height: 0,
157 reasons: LayerSurfaceReasons::default(),
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, Default, PartialEq)]
163pub struct FrameStatsSnapshot {
164 pub submits: u32,
165 pub offscreen_acquires: u32,
166 pub offscreen_news: u32,
167 pub offscreen_total_bytes: u64,
168 pub upload_bytes: u64,
169 pub isolated_layer_renders: u32,
170 pub isolated_layer_pixels: u64,
171 pub layer_cache_hits: u32,
172 pub layer_cache_misses: u32,
173 pub layer_cache_evictions: u32,
174 pub layer_cache_hit_pixels: u64,
175 pub layer_cache_miss_pixels: u64,
176 pub blur_passes: u32,
177 pub composite_passes: u32,
178 pub effect_applies: u32,
179 pub shape_passes: u32,
180 pub image_passes: u32,
181 pub text_passes: u32,
182 pub offscreen_pool_size: u32,
183 pub offscreen_pool_bytes: u64,
184 pub text_pool_size: u32,
185 pub layer_cache_size: u32,
186 pub layer_cache_bytes: u64,
187 pub image_cache_size: u32,
188 pub text_cache_size: u32,
189 pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
190 pub top_isolated_layer_count: usize,
191}
192
193impl FrameStatsSnapshot {
194 pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
195 self.top_isolated_layers
196 .into_iter()
197 .flatten()
198 .take(self.top_isolated_layer_count)
199 }
200
201 fn layer_cache_hit_rate(self) -> f64 {
202 let total = self.layer_cache_hits + self.layer_cache_misses;
203 if total > 0 {
204 (self.layer_cache_hits as f64 / total as f64) * 100.0
205 } else {
206 0.0
207 }
208 }
209
210 fn print(self, frame_count: u64) {
211 let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
212 let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
213 let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
214 let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
215 let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
216 let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
217 let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
218 eprintln!(
219 "[GPU f#{}] submits={} | offscreen: acq={} new={} {:.1}MB pool={}({:.1}MB) | \
220 uploads={:.2}MB | \
221 isolated_layers={} area={:.2}MP | \
222 layer_cache: hit={} miss={} {:.1}% evict={} hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) | \
223 blur={} composite={} effect={} | shape={} image={} text={} | \
224 caches: text_pool={} img={} txt={}",
225 frame_count,
226 self.submits,
227 self.offscreen_acquires,
228 self.offscreen_news,
229 mb,
230 self.offscreen_pool_size,
231 pool_mb,
232 upload_mb,
233 self.isolated_layer_renders,
234 isolated_layer_mpx,
235 self.layer_cache_hits,
236 self.layer_cache_misses,
237 self.layer_cache_hit_rate(),
238 self.layer_cache_evictions,
239 layer_cache_hit_mpx,
240 layer_cache_miss_mpx,
241 self.layer_cache_size,
242 layer_cache_mb,
243 self.blur_passes,
244 self.composite_passes,
245 self.effect_applies,
246 self.shape_passes,
247 self.image_passes,
248 self.text_passes,
249 self.text_pool_size,
250 self.image_cache_size,
251 self.text_cache_size,
252 );
253 for (index, layer) in self.top_isolated_layers().enumerate() {
254 eprintln!(
255 " [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{} reasons={}",
256 layer.node_id,
257 layer.logical_rect.x,
258 layer.logical_rect.y,
259 layer.logical_rect.width,
260 layer.logical_rect.height,
261 layer.width,
262 layer.height,
263 layer.reasons.display(),
264 );
265 }
266 }
267}
268
269#[derive(Default)]
272pub(crate) struct FrameStats {
273 pub submits: Cell<u32>,
274 pub offscreen_acquires: Cell<u32>,
275 pub offscreen_news: Cell<u32>,
276 pub offscreen_total_bytes: Cell<u64>,
277 pub upload_bytes: Cell<u64>,
278 pub isolated_layer_renders: Cell<u32>,
279 pub isolated_layer_pixels: Cell<u64>,
280 pub layer_cache_hits: Cell<u32>,
281 pub layer_cache_misses: Cell<u32>,
282 pub layer_cache_evictions: Cell<u32>,
283 pub layer_cache_hit_pixels: Cell<u64>,
284 pub layer_cache_miss_pixels: Cell<u64>,
285 pub blur_passes: Cell<u32>,
286 pub composite_passes: Cell<u32>,
287 pub effect_applies: Cell<u32>,
288 pub shape_passes: Cell<u32>,
289 pub image_passes: Cell<u32>,
290 pub text_passes: Cell<u32>,
291 pub offscreen_pool_size: Cell<u32>,
293 pub offscreen_pool_bytes: Cell<u64>,
294 pub text_pool_size: Cell<u32>,
295 pub layer_cache_size: Cell<u32>,
296 pub layer_cache_bytes: Cell<u64>,
297 pub image_cache_size: Cell<u32>,
298 pub text_cache_size: Cell<u32>,
299 top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
300 top_isolated_layer_count: Cell<usize>,
301}
302
303impl FrameStats {
304 pub fn bump_submits(&self) {
305 self.submits.set(self.submits.get() + 1);
306 }
307
308 pub fn record_offscreen_acquire(&self, width: u32, height: u32, is_new: bool) {
309 self.offscreen_acquires
310 .set(self.offscreen_acquires.get() + 1);
311 if is_new {
312 self.offscreen_news.set(self.offscreen_news.get() + 1);
313 }
314 self.offscreen_total_bytes
315 .set(self.offscreen_total_bytes.get() + (width as u64) * (height as u64) * 4);
316 }
317
318 pub fn record_upload_bytes(&self, bytes: u64) {
319 self.upload_bytes
320 .set(self.upload_bytes.get().saturating_add(bytes));
321 }
322
323 pub fn record_isolated_layer_render(
324 &self,
325 width: u32,
326 height: u32,
327 node_id: Option<NodeId>,
328 logical_rect: Rect,
329 reasons: LayerSurfaceReasons,
330 ) {
331 self.isolated_layer_renders
332 .set(self.isolated_layer_renders.get().saturating_add(1));
333 self.isolated_layer_pixels.set(
334 self.isolated_layer_pixels
335 .get()
336 .saturating_add((width as u64) * (height as u64)),
337 );
338 self.record_top_isolated_layer(IsolatedLayerStat {
339 node_id,
340 logical_rect,
341 width,
342 height,
343 reasons,
344 });
345 }
346
347 pub fn record_layer_cache_hit(&self, width: u32, height: u32) {
348 self.layer_cache_hits
349 .set(self.layer_cache_hits.get().saturating_add(1));
350 self.layer_cache_hit_pixels.set(
351 self.layer_cache_hit_pixels
352 .get()
353 .saturating_add((width as u64) * (height as u64)),
354 );
355 }
356
357 pub fn record_layer_cache_miss(&self, width: u32, height: u32) {
358 self.layer_cache_misses
359 .set(self.layer_cache_misses.get().saturating_add(1));
360 self.layer_cache_miss_pixels.set(
361 self.layer_cache_miss_pixels
362 .get()
363 .saturating_add((width as u64) * (height as u64)),
364 );
365 }
366
367 pub fn record_layer_cache_eviction(&self) {
368 self.layer_cache_evictions
369 .set(self.layer_cache_evictions.get().saturating_add(1));
370 }
371
372 pub fn bump_shapes(&self) {
373 self.shape_passes.set(self.shape_passes.get() + 1);
374 }
375
376 pub fn bump_images(&self) {
377 self.image_passes.set(self.image_passes.get() + 1);
378 }
379
380 pub fn bump_text(&self) {
381 self.text_passes.set(self.text_passes.get() + 1);
382 }
383
384 pub fn snapshot(&self) -> FrameStatsSnapshot {
385 FrameStatsSnapshot {
386 submits: self.submits.get(),
387 offscreen_acquires: self.offscreen_acquires.get(),
388 offscreen_news: self.offscreen_news.get(),
389 offscreen_total_bytes: self.offscreen_total_bytes.get(),
390 upload_bytes: self.upload_bytes.get(),
391 isolated_layer_renders: self.isolated_layer_renders.get(),
392 isolated_layer_pixels: self.isolated_layer_pixels.get(),
393 layer_cache_hits: self.layer_cache_hits.get(),
394 layer_cache_misses: self.layer_cache_misses.get(),
395 layer_cache_evictions: self.layer_cache_evictions.get(),
396 layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
397 layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
398 blur_passes: self.blur_passes.get(),
399 composite_passes: self.composite_passes.get(),
400 effect_applies: self.effect_applies.get(),
401 shape_passes: self.shape_passes.get(),
402 image_passes: self.image_passes.get(),
403 text_passes: self.text_passes.get(),
404 offscreen_pool_size: self.offscreen_pool_size.get(),
405 offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
406 text_pool_size: self.text_pool_size.get(),
407 layer_cache_size: self.layer_cache_size.get(),
408 layer_cache_bytes: self.layer_cache_bytes.get(),
409 image_cache_size: self.image_cache_size.get(),
410 text_cache_size: self.text_cache_size.get(),
411 top_isolated_layers: *self.top_isolated_layers.borrow(),
412 top_isolated_layer_count: self.top_isolated_layer_count.get(),
413 }
414 }
415
416 pub fn reset(&self) {
417 self.submits.set(0);
418 self.offscreen_acquires.set(0);
419 self.offscreen_news.set(0);
420 self.offscreen_total_bytes.set(0);
421 self.upload_bytes.set(0);
422 self.isolated_layer_renders.set(0);
423 self.isolated_layer_pixels.set(0);
424 self.layer_cache_hits.set(0);
425 self.layer_cache_misses.set(0);
426 self.layer_cache_evictions.set(0);
427 self.layer_cache_hit_pixels.set(0);
428 self.layer_cache_miss_pixels.set(0);
429 self.blur_passes.set(0);
430 self.composite_passes.set(0);
431 self.effect_applies.set(0);
432 self.shape_passes.set(0);
433 self.image_passes.set(0);
434 self.text_passes.set(0);
435 *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
436 self.top_isolated_layer_count.set(0);
437 }
438
439 pub fn maybe_print_snapshot(
440 &self,
441 snapshot: FrameStatsSnapshot,
442 frame_count: &mut u64,
443 enabled: bool,
444 ) {
445 if !enabled {
446 return;
447 }
448 *frame_count += 1;
449 if (*frame_count).is_multiple_of(60) {
450 snapshot.print(*frame_count);
451 }
452 }
453
454 fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
455 if !layer.reasons.has_any() {
456 return;
457 }
458
459 let mut top_layers = self.top_isolated_layers.borrow_mut();
460 let len = self.top_isolated_layer_count.get();
461 let insert_at = top_layers[..len]
462 .iter()
463 .enumerate()
464 .find_map(|(index, existing)| {
465 existing
466 .filter(|existing| layer.pixel_area() > existing.pixel_area())
467 .map(|_| index)
468 })
469 .unwrap_or(len);
470
471 if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
472 return;
473 }
474
475 let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
476 len + 1
477 } else {
478 TOP_ISOLATED_LAYER_LIMIT
479 };
480
481 let mut index = new_len.saturating_sub(1);
482 while index > insert_at {
483 top_layers[index] = top_layers[index - 1];
484 index -= 1;
485 }
486 top_layers[insert_at] = Some(layer);
487 self.top_isolated_layer_count.set(new_len);
488 }
489}
490
491pub(crate) fn gpu_stats_enabled() -> bool {
492 use std::sync::OnceLock;
493 static ENABLED: OnceLock<bool> = OnceLock::new();
494 *ENABLED.get_or_init(|| {
495 std::env::var("CRANPOSE_GPU_STATS")
496 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
497 .unwrap_or(false)
498 })
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn layer_cache_counters_accumulate_and_reset() {
507 let stats = FrameStats::default();
508 stats.record_upload_bytes(512);
509 stats.record_layer_cache_hit(10, 20);
510 stats.record_layer_cache_hit(3, 4);
511 stats.record_layer_cache_miss(5, 6);
512 stats.record_layer_cache_eviction();
513
514 assert_eq!(stats.layer_cache_hits.get(), 2);
515 assert_eq!(stats.layer_cache_misses.get(), 1);
516 assert_eq!(stats.layer_cache_evictions.get(), 1);
517 assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
518 assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
519
520 stats.record_isolated_layer_render(
521 7,
522 8,
523 Some(9),
524 Rect {
525 x: 2.0,
526 y: 3.0,
527 width: 4.0,
528 height: 5.0,
529 },
530 LayerSurfaceReasons {
531 immediate_shadow: true,
532 ..LayerSurfaceReasons::default()
533 },
534 );
535 let snapshot = stats.snapshot();
536
537 assert_eq!(snapshot.isolated_layer_renders, 1);
538 assert_eq!(snapshot.isolated_layer_pixels, 56);
539 assert_eq!(snapshot.upload_bytes, 512);
540 assert_eq!(snapshot.layer_cache_hits, 2);
541 assert_eq!(snapshot.layer_cache_misses, 1);
542 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
543 assert_eq!(top_layers.len(), 1);
544 assert_eq!(top_layers[0].node_id, Some(9));
545 assert_eq!(stats.layer_cache_hits.get(), 2);
546 assert_eq!(stats.layer_cache_misses.get(), 1);
547
548 stats.reset();
549
550 assert_eq!(stats.layer_cache_hits.get(), 0);
551 assert_eq!(stats.layer_cache_misses.get(), 0);
552 assert_eq!(stats.layer_cache_evictions.get(), 0);
553 assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
554 assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
555 assert_eq!(stats.upload_bytes.get(), 0);
556 assert_eq!(stats.isolated_layer_renders.get(), 0);
557 assert_eq!(stats.isolated_layer_pixels.get(), 0);
558 assert_eq!(stats.top_isolated_layer_count.get(), 0);
559 }
560
561 #[test]
562 fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
563 let stats = FrameStats::default();
564 let snapshot = stats.snapshot();
565 let mut frame_count = 0;
566
567 stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
568 assert_eq!(frame_count, 0);
569
570 stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
571 assert_eq!(frame_count, 1);
572 }
573
574 #[test]
575 fn layer_surface_reasons_report_runtime_only_bits() {
576 let reasons = LayerSurfaceReasons {
577 immediate_shadow: true,
578 mixed_direct_content: true,
579 ..LayerSurfaceReasons::default()
580 };
581
582 assert!(reasons.has_any());
583 assert!(reasons.has_renderer_forced_surface());
584 assert_eq!(
585 reasons.labels().collect::<Vec<_>>(),
586 vec!["immediate_shadow", "mixed_direct_content"]
587 );
588 assert_eq!(reasons.display(), "immediate_shadow+mixed_direct_content");
589 }
590
591 #[test]
592 fn mixed_direct_content_only_is_diagnostic_not_isolating() {
593 let reasons = LayerSurfaceReasons {
594 mixed_direct_content: true,
595 ..LayerSurfaceReasons::default()
596 };
597
598 assert!(!reasons.has_any());
599 assert!(!reasons.has_renderer_forced_surface());
600 assert_eq!(
601 reasons.labels().collect::<Vec<_>>(),
602 vec!["mixed_direct_content"]
603 );
604 assert_eq!(reasons.display(), "mixed_direct_content");
605 }
606
607 #[test]
608 fn has_any_matches_has_isolating_requirement_for_each_requirement() {
609 let all_requirements = [
610 SurfaceRequirement::ExplicitOffscreen,
611 SurfaceRequirement::RenderEffect,
612 SurfaceRequirement::Backdrop,
613 SurfaceRequirement::GroupOpacity,
614 SurfaceRequirement::BlendMode,
615 SurfaceRequirement::ImmediateShadow,
616 SurfaceRequirement::TextMaterialMask,
617 SurfaceRequirement::MotionStableCapture,
618 SurfaceRequirement::NonTranslationTransform,
619 SurfaceRequirement::MixedDirectContent,
620 ];
621 for requirement in all_requirements {
622 let set = SurfaceRequirementSet::default().with(requirement);
623 let reasons = LayerSurfaceReasons::from(set);
624 assert_eq!(
625 reasons.has_any(),
626 set.has_isolating_requirement(),
627 "has_any vs has_isolating_requirement mismatch for {requirement:?}"
628 );
629 }
630 }
631
632 #[test]
633 fn top_isolated_layers_keep_largest_runtime_surfaces() {
634 let stats = FrameStats::default();
635 for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
636 stats.record_isolated_layer_render(
637 16 + index as u32,
638 8 + index as u32,
639 Some(index),
640 Rect {
641 x: index as f32,
642 y: 0.0,
643 width: 10.0,
644 height: 10.0,
645 },
646 LayerSurfaceReasons {
647 immediate_shadow: true,
648 ..LayerSurfaceReasons::default()
649 },
650 );
651 }
652
653 let snapshot = stats.snapshot();
654 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
655 assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
656 assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
657 assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
658 }
659}