1use crate::frame_graph::FrameCommandStats;
7use crate::surface_requirements::{SurfaceRequirement, SurfaceRequirementSet};
8use std::cell::{Cell, RefCell};
9
10use cranpose_core::NodeId;
11use cranpose_ui_graphics::Rect;
12
13const TOP_ISOLATED_LAYER_LIMIT: usize = 8;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct LayerSurfaceReasons {
17 pub explicit_offscreen: bool,
18 pub effect: bool,
19 pub backdrop: bool,
20 pub group_opacity: bool,
21 pub blend_mode: bool,
22 pub shape_clip: bool,
23 pub immediate_shadow: bool,
24 pub text_local_surface: bool,
25 pub motion_stable_capture: bool,
26 pub mixed_direct_content: bool,
27 pub non_translation_transform: bool,
28 pub pixel_stable_composite: bool,
29}
30
31impl LayerSurfaceReasons {
32 pub fn has_any(self) -> bool {
37 self.explicit_offscreen
38 || self.effect
39 || self.backdrop
40 || self.group_opacity
41 || self.blend_mode
42 || self.shape_clip
43 || self.text_local_surface
44 || self.motion_stable_capture
45 || self.non_translation_transform
46 }
47
48 pub fn labels(self) -> impl Iterator<Item = &'static str> {
49 let mut labels = [None; 12];
50 let mut len = 0usize;
51
52 if self.explicit_offscreen {
53 labels[len] = Some("explicit_offscreen");
54 len += 1;
55 }
56 if self.effect {
57 labels[len] = Some("effect");
58 len += 1;
59 }
60 if self.backdrop {
61 labels[len] = Some("backdrop");
62 len += 1;
63 }
64 if self.group_opacity {
65 labels[len] = Some("group_opacity");
66 len += 1;
67 }
68 if self.blend_mode {
69 labels[len] = Some("blend_mode");
70 len += 1;
71 }
72 if self.shape_clip {
73 labels[len] = Some("shape_clip");
74 len += 1;
75 }
76 if self.immediate_shadow {
77 labels[len] = Some("immediate_shadow");
78 len += 1;
79 }
80 if self.text_local_surface {
81 labels[len] = Some("text_local_surface");
82 len += 1;
83 }
84 if self.motion_stable_capture {
85 labels[len] = Some("motion_stable_capture");
86 len += 1;
87 }
88 if self.mixed_direct_content {
89 labels[len] = Some("mixed_direct_content");
90 len += 1;
91 }
92 if self.non_translation_transform {
93 labels[len] = Some("non_translation_transform");
94 len += 1;
95 }
96 if self.pixel_stable_composite {
97 labels[len] = Some("pixel_stable_composite");
98 len += 1;
99 }
100
101 labels.into_iter().flatten().take(len)
102 }
103
104 pub fn display(self) -> String {
105 let mut joined = String::new();
106 for (index, label) in self.labels().enumerate() {
107 if index > 0 {
108 joined.push('+');
109 }
110 joined.push_str(label);
111 }
112 if joined.is_empty() {
113 joined.push_str("none");
114 }
115 joined
116 }
117
118 pub fn has_renderer_forced_surface(self) -> bool {
119 self.text_local_surface || self.non_translation_transform
120 }
121}
122
123impl From<SurfaceRequirementSet> for LayerSurfaceReasons {
124 fn from(requirements: SurfaceRequirementSet) -> Self {
125 Self {
126 explicit_offscreen: requirements.contains(SurfaceRequirement::ExplicitOffscreen),
127 effect: requirements.contains(SurfaceRequirement::RenderEffect),
128 backdrop: requirements.contains(SurfaceRequirement::Backdrop),
129 group_opacity: requirements.contains(SurfaceRequirement::GroupOpacity),
130 blend_mode: requirements.contains(SurfaceRequirement::BlendMode),
131 shape_clip: requirements.contains(SurfaceRequirement::ShapeClip),
132 immediate_shadow: requirements.contains(SurfaceRequirement::ImmediateShadow),
133 text_local_surface: requirements.contains(SurfaceRequirement::TextMaterialMask),
134 motion_stable_capture: requirements.contains(SurfaceRequirement::MotionStableCapture),
135 mixed_direct_content: requirements.contains(SurfaceRequirement::MixedDirectContent),
136 non_translation_transform: requirements
137 .contains(SurfaceRequirement::NonTranslationTransform),
138 pixel_stable_composite: requirements.contains(SurfaceRequirement::PixelStableComposite),
139 }
140 }
141}
142
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct IsolatedLayerStat {
145 pub node_id: Option<NodeId>,
146 pub logical_rect: Rect,
147 pub width: u32,
148 pub height: u32,
149 pub reasons: LayerSurfaceReasons,
150}
151
152impl IsolatedLayerStat {
153 fn pixel_area(self) -> u64 {
154 (self.width as u64) * (self.height as u64)
155 }
156}
157
158impl Default for IsolatedLayerStat {
159 fn default() -> Self {
160 Self {
161 node_id: None,
162 logical_rect: Rect {
163 x: 0.0,
164 y: 0.0,
165 width: 0.0,
166 height: 0.0,
167 },
168 width: 0,
169 height: 0,
170 reasons: LayerSurfaceReasons::default(),
171 }
172 }
173}
174
175#[derive(Clone, Copy, Debug, Default, PartialEq)]
176pub struct FrameStatsSnapshot {
177 pub submits: u32,
178 pub encoder_count: u32,
179 pub submit_count: u32,
180 pub pass_count: u32,
181 pub offscreen_acquires: u32,
182 pub offscreen_news: u32,
183 pub offscreen_total_bytes: u64,
184 pub transient_texture_bytes: u64,
185 pub retained_texture_bytes: u64,
186 pub upload_bytes: u64,
187 pub isolated_layer_renders: u32,
188 pub isolated_layer_pixels: u64,
189 pub layer_cache_hits: u32,
190 pub layer_cache_misses: u32,
191 pub layer_cache_evictions: u32,
192 pub layer_cache_hit_pixels: u64,
193 pub layer_cache_miss_pixels: u64,
194 pub shadow_shape_cache_hits: u32,
195 pub shadow_shape_cache_misses: u32,
196 pub shadow_shape_cache_hit_pixels: u64,
197 pub shadow_shape_cache_miss_pixels: u64,
198 pub shadow_text_blur_fallbacks: u32,
199 pub blur_passes: u32,
200 pub composite_passes: u32,
201 pub effect_applies: u32,
202 pub shape_passes: u32,
203 pub image_passes: u32,
204 pub text_passes: u32,
205 pub draw_calls: u32,
212 pub text_image_cache_hits: u32,
213 pub text_image_cache_misses: u32,
214 pub text_image_cache_hit_pixels: u64,
215 pub text_image_cache_miss_pixels: u64,
216 pub text_image_raster_bytes: u64,
217 pub text_glyph_atlas_hits: u32,
218 pub text_glyph_atlas_misses: u32,
219 pub text_glyph_atlas_miss_pixels: u64,
220 pub offscreen_pool_size: u32,
221 pub offscreen_pool_bytes: u64,
222 pub text_pool_size: u32,
223 pub layer_cache_size: u32,
224 pub layer_cache_bytes: u64,
225 pub image_cache_size: u32,
226 pub text_cache_size: u32,
227 pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
228 pub top_isolated_layer_count: usize,
229}
230
231impl FrameStatsSnapshot {
232 pub(crate) fn with_command_stats_added(mut self, stats: FrameCommandStats) -> Self {
233 self.submits = self.submits.saturating_add(stats.submit_count);
234 self.encoder_count = self.encoder_count.saturating_add(stats.encoder_count);
235 self.submit_count = self.submit_count.saturating_add(stats.submit_count);
236 self.pass_count = self.pass_count.saturating_add(stats.pass_count);
237 self.transient_texture_bytes = self
238 .transient_texture_bytes
239 .saturating_add(stats.transient_texture_bytes);
240 self.retained_texture_bytes = self
241 .retained_texture_bytes
242 .max(stats.retained_texture_bytes);
243 self.upload_bytes = self.upload_bytes.saturating_add(stats.upload_bytes);
244 self
245 }
246
247 pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
248 self.top_isolated_layers
249 .into_iter()
250 .flatten()
251 .take(self.top_isolated_layer_count)
252 }
253
254 fn layer_cache_hit_rate(self) -> f64 {
255 let total = self.layer_cache_hits + self.layer_cache_misses;
256 if total > 0 {
257 (self.layer_cache_hits as f64 / total as f64) * 100.0
258 } else {
259 0.0
260 }
261 }
262
263 fn print(self, frame_count: u64) {
264 let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
265 let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
266 let retained_mb = self.retained_texture_bytes as f64 / (1024.0 * 1024.0);
267 let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
268 let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
269 let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
270 let shadow_cache_hit_mpx = self.shadow_shape_cache_hit_pixels as f64 / 1_000_000.0;
271 let shadow_cache_miss_mpx = self.shadow_shape_cache_miss_pixels as f64 / 1_000_000.0;
272 let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
273 let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
274 eprintln!(
275 "[GPU f#{}] encoders={} submits={} passes={} | offscreen: acq={} new={} {:.1}MB pool={}({:.1}MB) retained={:.1}MB | \
276 uploads={:.2}MB | \
277 isolated_layers={} area={:.2}MP | \
278 layer_cache: hit={} miss={} {:.1}% evict={} hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) | \
279 shadow_cache: shape_hit={} shape_miss={} hit_px={:.2}MP miss_px={:.2}MP text_blur_fallback={} | \
280 blur={} composite={} effect={} | shape={} image={} text={} draws={} | \
281 text_img_cache: hit={} miss={} hit_px={:.2}MP miss_px={:.2}MP raster={:.2}MB | \
282 text_glyph_atlas: hit={} miss={} miss_px={:.2}MP | \
283 caches: text_pool={} img={} txt={}",
284 frame_count,
285 self.encoder_count,
286 self.submit_count,
287 self.pass_count,
288 self.offscreen_acquires,
289 self.offscreen_news,
290 mb,
291 self.offscreen_pool_size,
292 pool_mb,
293 retained_mb,
294 upload_mb,
295 self.isolated_layer_renders,
296 isolated_layer_mpx,
297 self.layer_cache_hits,
298 self.layer_cache_misses,
299 self.layer_cache_hit_rate(),
300 self.layer_cache_evictions,
301 layer_cache_hit_mpx,
302 layer_cache_miss_mpx,
303 self.layer_cache_size,
304 layer_cache_mb,
305 self.shadow_shape_cache_hits,
306 self.shadow_shape_cache_misses,
307 shadow_cache_hit_mpx,
308 shadow_cache_miss_mpx,
309 self.shadow_text_blur_fallbacks,
310 self.blur_passes,
311 self.composite_passes,
312 self.effect_applies,
313 self.shape_passes,
314 self.image_passes,
315 self.text_passes,
316 self.draw_calls,
317 self.text_image_cache_hits,
318 self.text_image_cache_misses,
319 self.text_image_cache_hit_pixels as f64 / 1_000_000.0,
320 self.text_image_cache_miss_pixels as f64 / 1_000_000.0,
321 self.text_image_raster_bytes as f64 / (1024.0 * 1024.0),
322 self.text_glyph_atlas_hits,
323 self.text_glyph_atlas_misses,
324 self.text_glyph_atlas_miss_pixels as f64 / 1_000_000.0,
325 self.text_pool_size,
326 self.image_cache_size,
327 self.text_cache_size,
328 );
329 for (index, layer) in self.top_isolated_layers().enumerate() {
330 eprintln!(
331 " [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{} reasons={}",
332 layer.node_id,
333 layer.logical_rect.x,
334 layer.logical_rect.y,
335 layer.logical_rect.width,
336 layer.logical_rect.height,
337 layer.width,
338 layer.height,
339 layer.reasons.display(),
340 );
341 }
342 }
343}
344
345#[derive(Default)]
348pub(crate) struct FrameStats {
349 pub submits: Cell<u32>,
350 pub command_encoder_count: Cell<u32>,
351 pub command_submit_count: Cell<u32>,
352 pub command_pass_count: Cell<u32>,
353 pub command_transient_texture_bytes: Cell<u64>,
354 pub command_retained_texture_bytes: Cell<u64>,
355 pub command_upload_bytes: Cell<u64>,
356 pub offscreen_acquires: Cell<u32>,
357 pub offscreen_news: Cell<u32>,
358 pub offscreen_total_bytes: Cell<u64>,
359 pub upload_bytes: Cell<u64>,
360 pub isolated_layer_renders: Cell<u32>,
361 pub isolated_layer_pixels: Cell<u64>,
362 pub layer_cache_hits: Cell<u32>,
363 pub layer_cache_misses: Cell<u32>,
364 pub layer_cache_evictions: Cell<u32>,
365 pub layer_cache_hit_pixels: Cell<u64>,
366 pub layer_cache_miss_pixels: Cell<u64>,
367 pub shadow_shape_cache_hits: Cell<u32>,
368 pub shadow_shape_cache_misses: Cell<u32>,
369 pub shadow_shape_cache_hit_pixels: Cell<u64>,
370 pub shadow_shape_cache_miss_pixels: Cell<u64>,
371 pub shadow_text_blur_fallbacks: Cell<u32>,
372 pub blur_passes: Cell<u32>,
373 pub composite_passes: Cell<u32>,
374 pub effect_applies: Cell<u32>,
375 pub shape_passes: Cell<u32>,
376 pub image_passes: Cell<u32>,
377 pub text_passes: Cell<u32>,
378 pub draw_calls: Cell<u32>,
379 pub text_image_cache_hits: Cell<u32>,
380 pub text_image_cache_misses: Cell<u32>,
381 pub text_image_cache_hit_pixels: Cell<u64>,
382 pub text_image_cache_miss_pixels: Cell<u64>,
383 pub text_image_raster_bytes: Cell<u64>,
384 pub text_glyph_atlas_hits: Cell<u32>,
385 pub text_glyph_atlas_misses: Cell<u32>,
386 pub text_glyph_atlas_miss_pixels: Cell<u64>,
387 pub offscreen_pool_size: Cell<u32>,
389 pub offscreen_pool_bytes: Cell<u64>,
390 pub text_pool_size: Cell<u32>,
391 pub layer_cache_size: Cell<u32>,
392 pub layer_cache_bytes: Cell<u64>,
393 pub image_cache_size: Cell<u32>,
394 pub text_cache_size: Cell<u32>,
395 top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
396 top_isolated_layer_count: Cell<usize>,
397 shadow_shape_cache_miss_log_count: Cell<u32>,
398}
399
400impl FrameStats {
401 pub fn record_command_stats(&self, stats: FrameCommandStats) {
402 self.submits
403 .set(self.submits.get().saturating_add(stats.submit_count));
404 self.command_encoder_count.set(
405 self.command_encoder_count
406 .get()
407 .saturating_add(stats.encoder_count),
408 );
409 self.command_submit_count.set(
410 self.command_submit_count
411 .get()
412 .saturating_add(stats.submit_count),
413 );
414 self.command_pass_count.set(
415 self.command_pass_count
416 .get()
417 .saturating_add(stats.pass_count),
418 );
419 self.command_transient_texture_bytes.set(
420 self.command_transient_texture_bytes
421 .get()
422 .saturating_add(stats.transient_texture_bytes),
423 );
424 self.command_retained_texture_bytes.set(
425 self.command_retained_texture_bytes
426 .get()
427 .max(stats.retained_texture_bytes),
428 );
429 self.command_upload_bytes.set(
430 self.command_upload_bytes
431 .get()
432 .saturating_add(stats.upload_bytes),
433 );
434 }
435
436 pub fn record_offscreen_acquire(&self, width: u32, height: u32, is_new: bool) {
437 self.offscreen_acquires
438 .set(self.offscreen_acquires.get() + 1);
439 if is_new {
440 self.offscreen_news.set(self.offscreen_news.get() + 1);
441 }
442 self.offscreen_total_bytes
443 .set(self.offscreen_total_bytes.get() + (width as u64) * (height as u64) * 4);
444 }
445
446 pub fn record_upload_bytes(&self, bytes: u64) {
447 self.upload_bytes
448 .set(self.upload_bytes.get().saturating_add(bytes));
449 }
450
451 pub fn record_isolated_layer_render(
452 &self,
453 width: u32,
454 height: u32,
455 node_id: Option<NodeId>,
456 logical_rect: Rect,
457 reasons: LayerSurfaceReasons,
458 ) {
459 self.isolated_layer_renders
460 .set(self.isolated_layer_renders.get().saturating_add(1));
461 self.isolated_layer_pixels.set(
462 self.isolated_layer_pixels
463 .get()
464 .saturating_add((width as u64) * (height as u64)),
465 );
466 self.record_top_isolated_layer(IsolatedLayerStat {
467 node_id,
468 logical_rect,
469 width,
470 height,
471 reasons,
472 });
473 }
474
475 pub fn record_layer_cache_hit(&self, width: u32, height: u32) {
476 self.layer_cache_hits
477 .set(self.layer_cache_hits.get().saturating_add(1));
478 self.layer_cache_hit_pixels.set(
479 self.layer_cache_hit_pixels
480 .get()
481 .saturating_add((width as u64) * (height as u64)),
482 );
483 }
484
485 pub fn record_layer_cache_miss(&self, width: u32, height: u32) {
486 self.layer_cache_misses
487 .set(self.layer_cache_misses.get().saturating_add(1));
488 self.layer_cache_miss_pixels.set(
489 self.layer_cache_miss_pixels
490 .get()
491 .saturating_add((width as u64) * (height as u64)),
492 );
493 }
494
495 pub fn record_layer_cache_eviction(&self) {
496 self.layer_cache_evictions
497 .set(self.layer_cache_evictions.get().saturating_add(1));
498 }
499
500 pub fn record_shadow_shape_cache_hit(&self, width: u32, height: u32) {
501 self.shadow_shape_cache_hits
502 .set(self.shadow_shape_cache_hits.get().saturating_add(1));
503 self.shadow_shape_cache_hit_pixels.set(
504 self.shadow_shape_cache_hit_pixels
505 .get()
506 .saturating_add((width as u64) * (height as u64)),
507 );
508 }
509
510 pub fn record_shadow_shape_cache_miss(&self, width: u32, height: u32) {
511 self.shadow_shape_cache_misses
512 .set(self.shadow_shape_cache_misses.get().saturating_add(1));
513 self.shadow_shape_cache_miss_pixels.set(
514 self.shadow_shape_cache_miss_pixels
515 .get()
516 .saturating_add((width as u64) * (height as u64)),
517 );
518 }
519
520 #[allow(clippy::too_many_arguments)]
521 pub fn maybe_print_shadow_shape_cache_miss(
522 &self,
523 width: u32,
524 height: u32,
525 content_hash: u64,
526 blur_radius: f32,
527 viewport_offset: [f32; 2],
528 shape_count: usize,
529 clip: Option<Rect>,
530 ) {
531 if !shadow_cache_diagnostics_enabled() {
532 return;
533 }
534
535 let count = self.shadow_shape_cache_miss_log_count.get();
536 if count >= 16 {
537 return;
538 }
539 self.shadow_shape_cache_miss_log_count.set(count + 1);
540
541 let clip_text = clip
542 .map(|clip| {
543 format!(
544 "({:.1},{:.1},{:.1},{:.1})",
545 clip.x, clip.y, clip.width, clip.height
546 )
547 })
548 .unwrap_or_else(|| "none".to_string());
549 eprintln!(
550 "[shadow-cache-miss #{count}] size={}x{} content_hash={content_hash} blur={:.2} viewport_offset=({:.1},{:.1}) shapes={} clip={}",
551 width,
552 height,
553 blur_radius,
554 viewport_offset[0],
555 viewport_offset[1],
556 shape_count,
557 clip_text,
558 );
559 }
560
561 pub fn record_shadow_text_blur_fallback(&self) {
562 self.shadow_text_blur_fallbacks
563 .set(self.shadow_text_blur_fallbacks.get().saturating_add(1));
564 }
565
566 pub fn bump_shapes(&self) {
567 self.shape_passes.set(self.shape_passes.get() + 1);
568 }
569
570 pub fn bump_images(&self) {
571 self.image_passes.set(self.image_passes.get() + 1);
572 }
573
574 pub fn add_draw_calls(&self, count: u32) {
578 self.draw_calls
579 .set(self.draw_calls.get().saturating_add(count));
580 }
581
582 pub fn bump_text(&self) {
583 self.text_passes.set(self.text_passes.get() + 1);
584 }
585
586 pub fn record_text_image_cache_hit(&self, width: u32, height: u32) {
587 self.text_image_cache_hits
588 .set(self.text_image_cache_hits.get().saturating_add(1));
589 self.text_image_cache_hit_pixels.set(
590 self.text_image_cache_hit_pixels
591 .get()
592 .saturating_add((width as u64) * (height as u64)),
593 );
594 }
595
596 pub fn record_text_image_cache_miss(&self, width: u32, height: u32) {
597 let pixels = (width as u64) * (height as u64);
598 self.text_image_cache_misses
599 .set(self.text_image_cache_misses.get().saturating_add(1));
600 self.text_image_cache_miss_pixels.set(
601 self.text_image_cache_miss_pixels
602 .get()
603 .saturating_add(pixels),
604 );
605 self.text_image_raster_bytes.set(
606 self.text_image_raster_bytes
607 .get()
608 .saturating_add(pixels * 4),
609 );
610 }
611
612 pub fn record_text_glyph_atlas_hit(&self) {
613 self.text_glyph_atlas_hits
614 .set(self.text_glyph_atlas_hits.get().saturating_add(1));
615 }
616
617 pub fn record_text_glyph_atlas_miss(&self, width: u32, height: u32) {
618 self.text_glyph_atlas_misses
619 .set(self.text_glyph_atlas_misses.get().saturating_add(1));
620 self.text_glyph_atlas_miss_pixels.set(
621 self.text_glyph_atlas_miss_pixels
622 .get()
623 .saturating_add((width as u64) * (height as u64)),
624 );
625 }
626
627 pub fn snapshot(&self) -> FrameStatsSnapshot {
628 let retained_texture_bytes = self
629 .offscreen_pool_bytes
630 .get()
631 .saturating_add(self.layer_cache_bytes.get());
632 FrameStatsSnapshot {
633 submits: self.submits.get(),
634 encoder_count: self.command_encoder_count.get(),
635 submit_count: self.command_submit_count.get(),
636 pass_count: self.command_pass_count.get(),
637 offscreen_acquires: self.offscreen_acquires.get(),
638 offscreen_news: self.offscreen_news.get(),
639 offscreen_total_bytes: self.offscreen_total_bytes.get(),
640 transient_texture_bytes: self
641 .offscreen_total_bytes
642 .get()
643 .saturating_add(self.command_transient_texture_bytes.get()),
644 retained_texture_bytes: retained_texture_bytes
645 .saturating_add(self.command_retained_texture_bytes.get()),
646 upload_bytes: self
647 .upload_bytes
648 .get()
649 .saturating_add(self.command_upload_bytes.get()),
650 isolated_layer_renders: self.isolated_layer_renders.get(),
651 isolated_layer_pixels: self.isolated_layer_pixels.get(),
652 layer_cache_hits: self.layer_cache_hits.get(),
653 layer_cache_misses: self.layer_cache_misses.get(),
654 layer_cache_evictions: self.layer_cache_evictions.get(),
655 layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
656 layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
657 shadow_shape_cache_hits: self.shadow_shape_cache_hits.get(),
658 shadow_shape_cache_misses: self.shadow_shape_cache_misses.get(),
659 shadow_shape_cache_hit_pixels: self.shadow_shape_cache_hit_pixels.get(),
660 shadow_shape_cache_miss_pixels: self.shadow_shape_cache_miss_pixels.get(),
661 shadow_text_blur_fallbacks: self.shadow_text_blur_fallbacks.get(),
662 blur_passes: self.blur_passes.get(),
663 composite_passes: self.composite_passes.get(),
664 effect_applies: self.effect_applies.get(),
665 shape_passes: self.shape_passes.get(),
666 image_passes: self.image_passes.get(),
667 text_passes: self.text_passes.get(),
668 draw_calls: self.draw_calls.get(),
669 text_image_cache_hits: self.text_image_cache_hits.get(),
670 text_image_cache_misses: self.text_image_cache_misses.get(),
671 text_image_cache_hit_pixels: self.text_image_cache_hit_pixels.get(),
672 text_image_cache_miss_pixels: self.text_image_cache_miss_pixels.get(),
673 text_image_raster_bytes: self.text_image_raster_bytes.get(),
674 text_glyph_atlas_hits: self.text_glyph_atlas_hits.get(),
675 text_glyph_atlas_misses: self.text_glyph_atlas_misses.get(),
676 text_glyph_atlas_miss_pixels: self.text_glyph_atlas_miss_pixels.get(),
677 offscreen_pool_size: self.offscreen_pool_size.get(),
678 offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
679 text_pool_size: self.text_pool_size.get(),
680 layer_cache_size: self.layer_cache_size.get(),
681 layer_cache_bytes: self.layer_cache_bytes.get(),
682 image_cache_size: self.image_cache_size.get(),
683 text_cache_size: self.text_cache_size.get(),
684 top_isolated_layers: *self.top_isolated_layers.borrow(),
685 top_isolated_layer_count: self.top_isolated_layer_count.get(),
686 }
687 }
688
689 pub fn reset(&self) {
690 self.submits.set(0);
691 self.command_encoder_count.set(0);
692 self.command_submit_count.set(0);
693 self.command_pass_count.set(0);
694 self.command_transient_texture_bytes.set(0);
695 self.command_retained_texture_bytes.set(0);
696 self.command_upload_bytes.set(0);
697 self.offscreen_acquires.set(0);
698 self.offscreen_news.set(0);
699 self.offscreen_total_bytes.set(0);
700 self.upload_bytes.set(0);
701 self.isolated_layer_renders.set(0);
702 self.isolated_layer_pixels.set(0);
703 self.layer_cache_hits.set(0);
704 self.layer_cache_misses.set(0);
705 self.layer_cache_evictions.set(0);
706 self.layer_cache_hit_pixels.set(0);
707 self.layer_cache_miss_pixels.set(0);
708 self.shadow_shape_cache_hits.set(0);
709 self.shadow_shape_cache_misses.set(0);
710 self.shadow_shape_cache_hit_pixels.set(0);
711 self.shadow_shape_cache_miss_pixels.set(0);
712 self.shadow_text_blur_fallbacks.set(0);
713 self.blur_passes.set(0);
714 self.composite_passes.set(0);
715 self.effect_applies.set(0);
716 self.shape_passes.set(0);
717 self.image_passes.set(0);
718 self.text_passes.set(0);
719 self.draw_calls.set(0);
720 self.text_image_cache_hits.set(0);
721 self.text_image_cache_misses.set(0);
722 self.text_image_cache_hit_pixels.set(0);
723 self.text_image_cache_miss_pixels.set(0);
724 self.text_image_raster_bytes.set(0);
725 self.text_glyph_atlas_hits.set(0);
726 self.text_glyph_atlas_misses.set(0);
727 self.text_glyph_atlas_miss_pixels.set(0);
728 *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
729 self.top_isolated_layer_count.set(0);
730 self.shadow_shape_cache_miss_log_count.set(0);
731 }
732
733 pub fn maybe_print_snapshot(
734 &self,
735 snapshot: FrameStatsSnapshot,
736 frame_count: &mut u64,
737 enabled: bool,
738 ) {
739 if !enabled {
740 return;
741 }
742 *frame_count += 1;
743 if (*frame_count).is_multiple_of(60) {
744 snapshot.print(*frame_count);
745 }
746 }
747
748 fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
749 if !layer.reasons.has_any() {
750 return;
751 }
752
753 let mut top_layers = self.top_isolated_layers.borrow_mut();
754 let len = self.top_isolated_layer_count.get();
755 let insert_at = top_layers[..len]
756 .iter()
757 .enumerate()
758 .find_map(|(index, existing)| {
759 existing
760 .filter(|existing| layer.pixel_area() > existing.pixel_area())
761 .map(|_| index)
762 })
763 .unwrap_or(len);
764
765 if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
766 return;
767 }
768
769 let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
770 len + 1
771 } else {
772 TOP_ISOLATED_LAYER_LIMIT
773 };
774
775 let mut index = new_len.saturating_sub(1);
776 while index > insert_at {
777 top_layers[index] = top_layers[index - 1];
778 index -= 1;
779 }
780 top_layers[insert_at] = Some(layer);
781 self.top_isolated_layer_count.set(new_len);
782 }
783}
784
785pub(crate) fn gpu_stats_enabled() -> bool {
786 std::env::var("CRANPOSE_GPU_STATS")
787 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
788 .unwrap_or(false)
789}
790
791pub(crate) fn print_gpu_memory_report(device: &wgpu::Device, frame_count: u64) {
808 let Some(report) = device.generate_allocator_report() else {
809 return;
810 };
811
812 const MB: f64 = 1024.0 * 1024.0;
813 let mut blocks = String::new();
814 for block in &report.blocks {
815 if !blocks.is_empty() {
816 blocks.push('+');
817 }
818 blocks.push_str(&format!("{:.1}", block.size as f64 / MB));
819 }
820
821 eprintln!(
822 "[GPU-MEM f#{}] reserved={:.1}MB allocated={:.1}MB blocks={}[{}MB] allocations={} | largest={:.6?}",
823 frame_count,
824 report.total_reserved_bytes as f64 / MB,
825 report.total_allocated_bytes as f64 / MB,
826 report.blocks.len(),
827 blocks,
828 report.allocations.len(),
829 report,
830 );
831}
832
833fn shadow_cache_diagnostics_enabled() -> bool {
834 std::env::var("CRANPOSE_GPU_SHADOW_CACHE_DIAG")
835 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
836 .unwrap_or(false)
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 #[test]
844 fn layer_cache_counters_accumulate_and_reset() {
845 let stats = FrameStats::default();
846 stats.record_upload_bytes(512);
847 stats.record_command_stats(FrameCommandStats {
848 encoder_count: 1,
849 submit_count: 1,
850 pass_count: 2,
851 transient_texture_bytes: 256,
852 retained_texture_bytes: 128,
853 upload_bytes: 64,
854 });
855 stats.bump_shapes();
856 stats.blur_passes.set(1);
857 stats.offscreen_total_bytes.set(1024);
858 stats.offscreen_pool_bytes.set(2048);
859 stats.record_layer_cache_hit(10, 20);
860 stats.record_layer_cache_hit(3, 4);
861 stats.record_layer_cache_miss(5, 6);
862 stats.record_layer_cache_eviction();
863 stats.record_shadow_shape_cache_hit(8, 9);
864 stats.record_shadow_shape_cache_miss(10, 11);
865 stats.record_shadow_text_blur_fallback();
866 stats.record_text_image_cache_hit(13, 17);
867 stats.record_text_image_cache_miss(19, 23);
868
869 assert_eq!(stats.layer_cache_hits.get(), 2);
870 assert_eq!(stats.layer_cache_misses.get(), 1);
871 assert_eq!(stats.layer_cache_evictions.get(), 1);
872 assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
873 assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
874 assert_eq!(stats.shadow_shape_cache_hits.get(), 1);
875 assert_eq!(stats.shadow_shape_cache_misses.get(), 1);
876 assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 72);
877 assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 110);
878 assert_eq!(stats.shadow_text_blur_fallbacks.get(), 1);
879
880 stats.record_isolated_layer_render(
881 7,
882 8,
883 Some(9),
884 Rect {
885 x: 2.0,
886 y: 3.0,
887 width: 4.0,
888 height: 5.0,
889 },
890 LayerSurfaceReasons {
891 text_local_surface: true,
892 ..LayerSurfaceReasons::default()
893 },
894 );
895 let snapshot = stats.snapshot();
896
897 assert_eq!(snapshot.isolated_layer_renders, 1);
898 assert_eq!(snapshot.isolated_layer_pixels, 56);
899 assert_eq!(snapshot.upload_bytes, 576);
900 assert_eq!(snapshot.encoder_count, 1);
901 assert_eq!(snapshot.submit_count, 1);
902 assert_eq!(snapshot.pass_count, 2);
903 assert_eq!(snapshot.transient_texture_bytes, 1280);
904 assert_eq!(snapshot.retained_texture_bytes, 2176);
905 assert_eq!(snapshot.layer_cache_hits, 2);
906 assert_eq!(snapshot.layer_cache_misses, 1);
907 assert_eq!(snapshot.shadow_shape_cache_hits, 1);
908 assert_eq!(snapshot.shadow_shape_cache_misses, 1);
909 assert_eq!(snapshot.shadow_shape_cache_hit_pixels, 72);
910 assert_eq!(snapshot.shadow_shape_cache_miss_pixels, 110);
911 assert_eq!(snapshot.shadow_text_blur_fallbacks, 1);
912 assert_eq!(snapshot.text_image_cache_hits, 1);
913 assert_eq!(snapshot.text_image_cache_misses, 1);
914 assert_eq!(snapshot.text_image_cache_hit_pixels, 221);
915 assert_eq!(snapshot.text_image_cache_miss_pixels, 437);
916 assert_eq!(snapshot.text_image_raster_bytes, 1748);
917 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
918 assert_eq!(top_layers.len(), 1);
919 assert_eq!(top_layers[0].node_id, Some(9));
920 assert_eq!(stats.layer_cache_hits.get(), 2);
921 assert_eq!(stats.layer_cache_misses.get(), 1);
922
923 stats.reset();
924
925 assert_eq!(stats.layer_cache_hits.get(), 0);
926 assert_eq!(stats.layer_cache_misses.get(), 0);
927 assert_eq!(stats.layer_cache_evictions.get(), 0);
928 assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
929 assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
930 assert_eq!(stats.shadow_shape_cache_hits.get(), 0);
931 assert_eq!(stats.shadow_shape_cache_misses.get(), 0);
932 assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 0);
933 assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 0);
934 assert_eq!(stats.shadow_text_blur_fallbacks.get(), 0);
935 assert_eq!(stats.text_image_cache_hits.get(), 0);
936 assert_eq!(stats.text_image_cache_misses.get(), 0);
937 assert_eq!(stats.text_image_cache_hit_pixels.get(), 0);
938 assert_eq!(stats.text_image_cache_miss_pixels.get(), 0);
939 assert_eq!(stats.text_image_raster_bytes.get(), 0);
940 assert_eq!(stats.upload_bytes.get(), 0);
941 assert_eq!(stats.isolated_layer_renders.get(), 0);
942 assert_eq!(stats.isolated_layer_pixels.get(), 0);
943 assert_eq!(stats.top_isolated_layer_count.get(), 0);
944 }
945
946 #[test]
947 fn command_stats_accumulate_and_reset() {
948 let stats = FrameStats::default();
949
950 stats.record_command_stats(FrameCommandStats {
951 encoder_count: 2,
952 submit_count: 2,
953 pass_count: 5,
954 transient_texture_bytes: 1024,
955 retained_texture_bytes: 2048,
956 upload_bytes: 512,
957 });
958 stats.bump_shapes();
959
960 let snapshot = stats.snapshot();
961 assert_eq!(snapshot.submits, 2);
962 assert_eq!(snapshot.encoder_count, 2);
963 assert_eq!(snapshot.submit_count, 2);
964 assert_eq!(snapshot.pass_count, 5);
965 assert_eq!(snapshot.transient_texture_bytes, 1024);
966 assert_eq!(snapshot.retained_texture_bytes, 2048);
967 assert_eq!(snapshot.upload_bytes, 512);
968
969 stats.reset();
970 let reset = stats.snapshot();
971 assert_eq!(reset.submits, 0);
972 assert_eq!(reset.encoder_count, 0);
973 assert_eq!(reset.submit_count, 0);
974 assert_eq!(reset.pass_count, 0);
975 assert_eq!(reset.transient_texture_bytes, 0);
976 assert_eq!(reset.retained_texture_bytes, 0);
977 assert_eq!(reset.upload_bytes, 0);
978 }
979
980 #[test]
981 fn snapshot_adds_explicit_readback_command_stats() {
982 let stats = FrameStats::default();
983 stats.record_command_stats(FrameCommandStats {
984 encoder_count: 1,
985 submit_count: 1,
986 pass_count: 2,
987 transient_texture_bytes: 128,
988 retained_texture_bytes: 512,
989 upload_bytes: 64,
990 });
991 let snapshot = stats
992 .snapshot()
993 .with_command_stats_added(FrameCommandStats {
994 encoder_count: 1,
995 submit_count: 1,
996 pass_count: 1,
997 transient_texture_bytes: 0,
998 retained_texture_bytes: 0,
999 upload_bytes: 0,
1000 });
1001
1002 assert_eq!(snapshot.submits, 2);
1003 assert_eq!(snapshot.encoder_count, 2);
1004 assert_eq!(snapshot.submit_count, 2);
1005 assert_eq!(snapshot.pass_count, 3);
1006 assert_eq!(snapshot.transient_texture_bytes, 128);
1007 assert_eq!(snapshot.retained_texture_bytes, 512);
1008 assert_eq!(snapshot.upload_bytes, 64);
1009 }
1010
1011 #[test]
1012 fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
1013 let stats = FrameStats::default();
1014 let snapshot = stats.snapshot();
1015 let mut frame_count = 0;
1016
1017 stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
1018 assert_eq!(frame_count, 0);
1019
1020 stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
1021 assert_eq!(frame_count, 1);
1022 }
1023
1024 #[test]
1025 fn layer_surface_reasons_report_runtime_only_bits() {
1026 let reasons = LayerSurfaceReasons {
1027 immediate_shadow: true,
1028 text_local_surface: true,
1029 mixed_direct_content: true,
1030 ..LayerSurfaceReasons::default()
1031 };
1032
1033 assert!(reasons.has_any());
1034 assert!(reasons.has_renderer_forced_surface());
1035 assert_eq!(
1036 reasons.labels().collect::<Vec<_>>(),
1037 vec![
1038 "immediate_shadow",
1039 "text_local_surface",
1040 "mixed_direct_content"
1041 ]
1042 );
1043 assert_eq!(
1044 reasons.display(),
1045 "immediate_shadow+text_local_surface+mixed_direct_content"
1046 );
1047 }
1048
1049 #[test]
1050 fn immediate_shadow_only_is_diagnostic_not_isolating() {
1051 let reasons = LayerSurfaceReasons {
1052 immediate_shadow: true,
1053 ..LayerSurfaceReasons::default()
1054 };
1055
1056 assert!(!reasons.has_any());
1057 assert!(!reasons.has_renderer_forced_surface());
1058 assert_eq!(
1059 reasons.labels().collect::<Vec<_>>(),
1060 vec!["immediate_shadow"]
1061 );
1062 assert_eq!(reasons.display(), "immediate_shadow");
1063 }
1064
1065 #[test]
1066 fn mixed_direct_content_only_is_diagnostic_not_isolating() {
1067 let reasons = LayerSurfaceReasons {
1068 mixed_direct_content: true,
1069 ..LayerSurfaceReasons::default()
1070 };
1071
1072 assert!(!reasons.has_any());
1073 assert!(!reasons.has_renderer_forced_surface());
1074 assert_eq!(
1075 reasons.labels().collect::<Vec<_>>(),
1076 vec!["mixed_direct_content"]
1077 );
1078 assert_eq!(reasons.display(), "mixed_direct_content");
1079 }
1080
1081 #[test]
1082 fn has_any_matches_has_isolating_requirement_for_each_requirement() {
1083 let all_requirements = [
1084 SurfaceRequirement::ExplicitOffscreen,
1085 SurfaceRequirement::RenderEffect,
1086 SurfaceRequirement::Backdrop,
1087 SurfaceRequirement::GroupOpacity,
1088 SurfaceRequirement::BlendMode,
1089 SurfaceRequirement::ShapeClip,
1090 SurfaceRequirement::ImmediateShadow,
1091 SurfaceRequirement::TextMaterialMask,
1092 SurfaceRequirement::MotionStableCapture,
1093 SurfaceRequirement::NonTranslationTransform,
1094 SurfaceRequirement::MixedDirectContent,
1095 SurfaceRequirement::PixelStableComposite,
1096 ];
1097 for requirement in all_requirements {
1098 let set = SurfaceRequirementSet::default().with(requirement);
1099 let reasons = LayerSurfaceReasons::from(set);
1100 assert_eq!(
1101 reasons.has_any(),
1102 set.has_isolating_requirement(),
1103 "has_any vs has_isolating_requirement mismatch for {requirement:?}"
1104 );
1105 }
1106 }
1107
1108 #[test]
1109 fn top_isolated_layers_keep_largest_runtime_surfaces() {
1110 let stats = FrameStats::default();
1111 for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
1112 stats.record_isolated_layer_render(
1113 16 + index as u32,
1114 8 + index as u32,
1115 Some(index),
1116 Rect {
1117 x: index as f32,
1118 y: 0.0,
1119 width: 10.0,
1120 height: 10.0,
1121 },
1122 LayerSurfaceReasons {
1123 text_local_surface: true,
1124 ..LayerSurfaceReasons::default()
1125 },
1126 );
1127 }
1128
1129 let snapshot = stats.snapshot();
1130 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
1131 assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
1132 assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
1133 assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
1134 }
1135}