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(
437 &self,
438 width: u32,
439 height: u32,
440 format: wgpu::TextureFormat,
441 is_new: bool,
442 ) {
443 self.offscreen_acquires
444 .set(self.offscreen_acquires.get() + 1);
445 if is_new {
446 self.offscreen_news.set(self.offscreen_news.get() + 1);
447 }
448 self.offscreen_total_bytes.set(
449 self.offscreen_total_bytes.get()
450 + (width as u64)
451 * (height as u64)
452 * crate::frame_graph::texture_format_bytes_per_pixel(format),
453 );
454 }
455
456 pub fn record_upload_bytes(&self, bytes: u64) {
457 self.upload_bytes
458 .set(self.upload_bytes.get().saturating_add(bytes));
459 }
460
461 pub fn record_isolated_layer_render(
462 &self,
463 width: u32,
464 height: u32,
465 node_id: Option<NodeId>,
466 logical_rect: Rect,
467 reasons: LayerSurfaceReasons,
468 ) {
469 self.isolated_layer_renders
470 .set(self.isolated_layer_renders.get().saturating_add(1));
471 self.isolated_layer_pixels.set(
472 self.isolated_layer_pixels
473 .get()
474 .saturating_add((width as u64) * (height as u64)),
475 );
476 self.record_top_isolated_layer(IsolatedLayerStat {
477 node_id,
478 logical_rect,
479 width,
480 height,
481 reasons,
482 });
483 }
484
485 pub fn record_layer_cache_hit(&self, width: u32, height: u32) {
486 self.layer_cache_hits
487 .set(self.layer_cache_hits.get().saturating_add(1));
488 self.layer_cache_hit_pixels.set(
489 self.layer_cache_hit_pixels
490 .get()
491 .saturating_add((width as u64) * (height as u64)),
492 );
493 }
494
495 pub fn record_layer_cache_miss(&self, width: u32, height: u32) {
496 self.layer_cache_misses
497 .set(self.layer_cache_misses.get().saturating_add(1));
498 self.layer_cache_miss_pixels.set(
499 self.layer_cache_miss_pixels
500 .get()
501 .saturating_add((width as u64) * (height as u64)),
502 );
503 }
504
505 pub fn record_layer_cache_eviction(&self) {
506 self.layer_cache_evictions
507 .set(self.layer_cache_evictions.get().saturating_add(1));
508 }
509
510 pub fn record_shadow_shape_cache_hit(&self, width: u32, height: u32) {
511 self.shadow_shape_cache_hits
512 .set(self.shadow_shape_cache_hits.get().saturating_add(1));
513 self.shadow_shape_cache_hit_pixels.set(
514 self.shadow_shape_cache_hit_pixels
515 .get()
516 .saturating_add((width as u64) * (height as u64)),
517 );
518 }
519
520 pub fn record_shadow_shape_cache_miss(&self, width: u32, height: u32) {
521 self.shadow_shape_cache_misses
522 .set(self.shadow_shape_cache_misses.get().saturating_add(1));
523 self.shadow_shape_cache_miss_pixels.set(
524 self.shadow_shape_cache_miss_pixels
525 .get()
526 .saturating_add((width as u64) * (height as u64)),
527 );
528 }
529
530 #[allow(clippy::too_many_arguments)]
531 pub fn maybe_print_shadow_shape_cache_miss(
532 &self,
533 width: u32,
534 height: u32,
535 content_hash: u64,
536 blur_radius: f32,
537 viewport_offset: [f32; 2],
538 shape_count: usize,
539 clip: Option<Rect>,
540 ) {
541 if !shadow_cache_diagnostics_enabled() {
542 return;
543 }
544
545 let count = self.shadow_shape_cache_miss_log_count.get();
546 if count >= 16 {
547 return;
548 }
549 self.shadow_shape_cache_miss_log_count.set(count + 1);
550
551 let clip_text = clip
552 .map(|clip| {
553 format!(
554 "({:.1},{:.1},{:.1},{:.1})",
555 clip.x, clip.y, clip.width, clip.height
556 )
557 })
558 .unwrap_or_else(|| "none".to_string());
559 eprintln!(
560 "[shadow-cache-miss #{count}] size={}x{} content_hash={content_hash} blur={:.2} viewport_offset=({:.1},{:.1}) shapes={} clip={}",
561 width,
562 height,
563 blur_radius,
564 viewport_offset[0],
565 viewport_offset[1],
566 shape_count,
567 clip_text,
568 );
569 }
570
571 pub fn record_shadow_text_blur_fallback(&self) {
572 self.shadow_text_blur_fallbacks
573 .set(self.shadow_text_blur_fallbacks.get().saturating_add(1));
574 }
575
576 pub fn bump_shapes(&self) {
577 self.shape_passes.set(self.shape_passes.get() + 1);
578 }
579
580 pub fn bump_images(&self) {
581 self.image_passes.set(self.image_passes.get() + 1);
582 }
583
584 pub fn add_draw_calls(&self, count: u32) {
588 self.draw_calls
589 .set(self.draw_calls.get().saturating_add(count));
590 }
591
592 pub fn bump_text(&self) {
593 self.text_passes.set(self.text_passes.get() + 1);
594 }
595
596 pub fn record_text_image_cache_hit(&self, width: u32, height: u32) {
597 self.text_image_cache_hits
598 .set(self.text_image_cache_hits.get().saturating_add(1));
599 self.text_image_cache_hit_pixels.set(
600 self.text_image_cache_hit_pixels
601 .get()
602 .saturating_add((width as u64) * (height as u64)),
603 );
604 }
605
606 pub fn record_text_image_cache_miss(&self, width: u32, height: u32) {
607 let pixels = (width as u64) * (height as u64);
608 self.text_image_cache_misses
609 .set(self.text_image_cache_misses.get().saturating_add(1));
610 self.text_image_cache_miss_pixels.set(
611 self.text_image_cache_miss_pixels
612 .get()
613 .saturating_add(pixels),
614 );
615 self.text_image_raster_bytes.set(
616 self.text_image_raster_bytes
617 .get()
618 .saturating_add(pixels * 4),
619 );
620 }
621
622 pub fn record_text_glyph_atlas_hit(&self) {
623 self.text_glyph_atlas_hits
624 .set(self.text_glyph_atlas_hits.get().saturating_add(1));
625 }
626
627 pub fn record_text_glyph_atlas_miss(&self, width: u32, height: u32) {
628 self.text_glyph_atlas_misses
629 .set(self.text_glyph_atlas_misses.get().saturating_add(1));
630 self.text_glyph_atlas_miss_pixels.set(
631 self.text_glyph_atlas_miss_pixels
632 .get()
633 .saturating_add((width as u64) * (height as u64)),
634 );
635 }
636
637 pub fn snapshot(&self) -> FrameStatsSnapshot {
638 let retained_texture_bytes = self
639 .offscreen_pool_bytes
640 .get()
641 .saturating_add(self.layer_cache_bytes.get());
642 FrameStatsSnapshot {
643 submits: self.submits.get(),
644 encoder_count: self.command_encoder_count.get(),
645 submit_count: self.command_submit_count.get(),
646 pass_count: self.command_pass_count.get(),
647 offscreen_acquires: self.offscreen_acquires.get(),
648 offscreen_news: self.offscreen_news.get(),
649 offscreen_total_bytes: self.offscreen_total_bytes.get(),
650 transient_texture_bytes: self
651 .offscreen_total_bytes
652 .get()
653 .saturating_add(self.command_transient_texture_bytes.get()),
654 retained_texture_bytes: retained_texture_bytes
655 .saturating_add(self.command_retained_texture_bytes.get()),
656 upload_bytes: self
657 .upload_bytes
658 .get()
659 .saturating_add(self.command_upload_bytes.get()),
660 isolated_layer_renders: self.isolated_layer_renders.get(),
661 isolated_layer_pixels: self.isolated_layer_pixels.get(),
662 layer_cache_hits: self.layer_cache_hits.get(),
663 layer_cache_misses: self.layer_cache_misses.get(),
664 layer_cache_evictions: self.layer_cache_evictions.get(),
665 layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
666 layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
667 shadow_shape_cache_hits: self.shadow_shape_cache_hits.get(),
668 shadow_shape_cache_misses: self.shadow_shape_cache_misses.get(),
669 shadow_shape_cache_hit_pixels: self.shadow_shape_cache_hit_pixels.get(),
670 shadow_shape_cache_miss_pixels: self.shadow_shape_cache_miss_pixels.get(),
671 shadow_text_blur_fallbacks: self.shadow_text_blur_fallbacks.get(),
672 blur_passes: self.blur_passes.get(),
673 composite_passes: self.composite_passes.get(),
674 effect_applies: self.effect_applies.get(),
675 shape_passes: self.shape_passes.get(),
676 image_passes: self.image_passes.get(),
677 text_passes: self.text_passes.get(),
678 draw_calls: self.draw_calls.get(),
679 text_image_cache_hits: self.text_image_cache_hits.get(),
680 text_image_cache_misses: self.text_image_cache_misses.get(),
681 text_image_cache_hit_pixels: self.text_image_cache_hit_pixels.get(),
682 text_image_cache_miss_pixels: self.text_image_cache_miss_pixels.get(),
683 text_image_raster_bytes: self.text_image_raster_bytes.get(),
684 text_glyph_atlas_hits: self.text_glyph_atlas_hits.get(),
685 text_glyph_atlas_misses: self.text_glyph_atlas_misses.get(),
686 text_glyph_atlas_miss_pixels: self.text_glyph_atlas_miss_pixels.get(),
687 offscreen_pool_size: self.offscreen_pool_size.get(),
688 offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
689 text_pool_size: self.text_pool_size.get(),
690 layer_cache_size: self.layer_cache_size.get(),
691 layer_cache_bytes: self.layer_cache_bytes.get(),
692 image_cache_size: self.image_cache_size.get(),
693 text_cache_size: self.text_cache_size.get(),
694 top_isolated_layers: *self.top_isolated_layers.borrow(),
695 top_isolated_layer_count: self.top_isolated_layer_count.get(),
696 }
697 }
698
699 pub fn reset(&self) {
700 self.submits.set(0);
701 self.command_encoder_count.set(0);
702 self.command_submit_count.set(0);
703 self.command_pass_count.set(0);
704 self.command_transient_texture_bytes.set(0);
705 self.command_retained_texture_bytes.set(0);
706 self.command_upload_bytes.set(0);
707 self.offscreen_acquires.set(0);
708 self.offscreen_news.set(0);
709 self.offscreen_total_bytes.set(0);
710 self.upload_bytes.set(0);
711 self.isolated_layer_renders.set(0);
712 self.isolated_layer_pixels.set(0);
713 self.layer_cache_hits.set(0);
714 self.layer_cache_misses.set(0);
715 self.layer_cache_evictions.set(0);
716 self.layer_cache_hit_pixels.set(0);
717 self.layer_cache_miss_pixels.set(0);
718 self.shadow_shape_cache_hits.set(0);
719 self.shadow_shape_cache_misses.set(0);
720 self.shadow_shape_cache_hit_pixels.set(0);
721 self.shadow_shape_cache_miss_pixels.set(0);
722 self.shadow_text_blur_fallbacks.set(0);
723 self.blur_passes.set(0);
724 self.composite_passes.set(0);
725 self.effect_applies.set(0);
726 self.shape_passes.set(0);
727 self.image_passes.set(0);
728 self.text_passes.set(0);
729 self.draw_calls.set(0);
730 self.text_image_cache_hits.set(0);
731 self.text_image_cache_misses.set(0);
732 self.text_image_cache_hit_pixels.set(0);
733 self.text_image_cache_miss_pixels.set(0);
734 self.text_image_raster_bytes.set(0);
735 self.text_glyph_atlas_hits.set(0);
736 self.text_glyph_atlas_misses.set(0);
737 self.text_glyph_atlas_miss_pixels.set(0);
738 *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
739 self.top_isolated_layer_count.set(0);
740 self.shadow_shape_cache_miss_log_count.set(0);
741 }
742
743 pub fn maybe_print_snapshot(
744 &self,
745 snapshot: FrameStatsSnapshot,
746 frame_count: &mut u64,
747 enabled: bool,
748 ) {
749 if !enabled {
750 return;
751 }
752 *frame_count += 1;
753 if (*frame_count).is_multiple_of(60) {
754 snapshot.print(*frame_count);
755 }
756 }
757
758 fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
759 if !layer.reasons.has_any() {
760 return;
761 }
762
763 let mut top_layers = self.top_isolated_layers.borrow_mut();
764 let len = self.top_isolated_layer_count.get();
765 let insert_at = top_layers[..len]
766 .iter()
767 .enumerate()
768 .find_map(|(index, existing)| {
769 existing
770 .filter(|existing| layer.pixel_area() > existing.pixel_area())
771 .map(|_| index)
772 })
773 .unwrap_or(len);
774
775 if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
776 return;
777 }
778
779 let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
780 len + 1
781 } else {
782 TOP_ISOLATED_LAYER_LIMIT
783 };
784
785 let mut index = new_len.saturating_sub(1);
786 while index > insert_at {
787 top_layers[index] = top_layers[index - 1];
788 index -= 1;
789 }
790 top_layers[insert_at] = Some(layer);
791 self.top_isolated_layer_count.set(new_len);
792 }
793}
794
795pub(crate) fn gpu_stats_enabled() -> bool {
796 std::env::var("CRANPOSE_GPU_STATS")
797 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
798 .unwrap_or(false)
799}
800
801pub(crate) fn print_gpu_memory_report(device: &wgpu::Device, frame_count: u64) {
818 let Some(report) = device.generate_allocator_report() else {
819 return;
820 };
821
822 const MB: f64 = 1024.0 * 1024.0;
823 let mut blocks = String::new();
824 for block in &report.blocks {
825 if !blocks.is_empty() {
826 blocks.push('+');
827 }
828 blocks.push_str(&format!("{:.1}", block.size as f64 / MB));
829 }
830
831 eprintln!(
832 "[GPU-MEM f#{}] reserved={:.1}MB allocated={:.1}MB blocks={}[{}MB] allocations={} | largest={:.6?}",
833 frame_count,
834 report.total_reserved_bytes as f64 / MB,
835 report.total_allocated_bytes as f64 / MB,
836 report.blocks.len(),
837 blocks,
838 report.allocations.len(),
839 report,
840 );
841}
842
843fn shadow_cache_diagnostics_enabled() -> bool {
844 std::env::var("CRANPOSE_GPU_SHADOW_CACHE_DIAG")
845 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
846 .unwrap_or(false)
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
854 fn layer_cache_counters_accumulate_and_reset() {
855 let stats = FrameStats::default();
856 stats.record_upload_bytes(512);
857 stats.record_command_stats(FrameCommandStats {
858 encoder_count: 1,
859 submit_count: 1,
860 pass_count: 2,
861 transient_texture_bytes: 256,
862 retained_texture_bytes: 128,
863 upload_bytes: 64,
864 });
865 stats.bump_shapes();
866 stats.blur_passes.set(1);
867 stats.offscreen_total_bytes.set(1024);
868 stats.offscreen_pool_bytes.set(2048);
869 stats.record_layer_cache_hit(10, 20);
870 stats.record_layer_cache_hit(3, 4);
871 stats.record_layer_cache_miss(5, 6);
872 stats.record_layer_cache_eviction();
873 stats.record_shadow_shape_cache_hit(8, 9);
874 stats.record_shadow_shape_cache_miss(10, 11);
875 stats.record_shadow_text_blur_fallback();
876 stats.record_text_image_cache_hit(13, 17);
877 stats.record_text_image_cache_miss(19, 23);
878
879 assert_eq!(stats.layer_cache_hits.get(), 2);
880 assert_eq!(stats.layer_cache_misses.get(), 1);
881 assert_eq!(stats.layer_cache_evictions.get(), 1);
882 assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
883 assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
884 assert_eq!(stats.shadow_shape_cache_hits.get(), 1);
885 assert_eq!(stats.shadow_shape_cache_misses.get(), 1);
886 assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 72);
887 assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 110);
888 assert_eq!(stats.shadow_text_blur_fallbacks.get(), 1);
889
890 stats.record_isolated_layer_render(
891 7,
892 8,
893 Some(9),
894 Rect {
895 x: 2.0,
896 y: 3.0,
897 width: 4.0,
898 height: 5.0,
899 },
900 LayerSurfaceReasons {
901 text_local_surface: true,
902 ..LayerSurfaceReasons::default()
903 },
904 );
905 let snapshot = stats.snapshot();
906
907 assert_eq!(snapshot.isolated_layer_renders, 1);
908 assert_eq!(snapshot.isolated_layer_pixels, 56);
909 assert_eq!(snapshot.upload_bytes, 576);
910 assert_eq!(snapshot.encoder_count, 1);
911 assert_eq!(snapshot.submit_count, 1);
912 assert_eq!(snapshot.pass_count, 2);
913 assert_eq!(snapshot.transient_texture_bytes, 1280);
914 assert_eq!(snapshot.retained_texture_bytes, 2176);
915 assert_eq!(snapshot.layer_cache_hits, 2);
916 assert_eq!(snapshot.layer_cache_misses, 1);
917 assert_eq!(snapshot.shadow_shape_cache_hits, 1);
918 assert_eq!(snapshot.shadow_shape_cache_misses, 1);
919 assert_eq!(snapshot.shadow_shape_cache_hit_pixels, 72);
920 assert_eq!(snapshot.shadow_shape_cache_miss_pixels, 110);
921 assert_eq!(snapshot.shadow_text_blur_fallbacks, 1);
922 assert_eq!(snapshot.text_image_cache_hits, 1);
923 assert_eq!(snapshot.text_image_cache_misses, 1);
924 assert_eq!(snapshot.text_image_cache_hit_pixels, 221);
925 assert_eq!(snapshot.text_image_cache_miss_pixels, 437);
926 assert_eq!(snapshot.text_image_raster_bytes, 1748);
927 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
928 assert_eq!(top_layers.len(), 1);
929 assert_eq!(top_layers[0].node_id, Some(9));
930 assert_eq!(stats.layer_cache_hits.get(), 2);
931 assert_eq!(stats.layer_cache_misses.get(), 1);
932
933 stats.reset();
934
935 assert_eq!(stats.layer_cache_hits.get(), 0);
936 assert_eq!(stats.layer_cache_misses.get(), 0);
937 assert_eq!(stats.layer_cache_evictions.get(), 0);
938 assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
939 assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
940 assert_eq!(stats.shadow_shape_cache_hits.get(), 0);
941 assert_eq!(stats.shadow_shape_cache_misses.get(), 0);
942 assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 0);
943 assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 0);
944 assert_eq!(stats.shadow_text_blur_fallbacks.get(), 0);
945 assert_eq!(stats.text_image_cache_hits.get(), 0);
946 assert_eq!(stats.text_image_cache_misses.get(), 0);
947 assert_eq!(stats.text_image_cache_hit_pixels.get(), 0);
948 assert_eq!(stats.text_image_cache_miss_pixels.get(), 0);
949 assert_eq!(stats.text_image_raster_bytes.get(), 0);
950 assert_eq!(stats.upload_bytes.get(), 0);
951 assert_eq!(stats.isolated_layer_renders.get(), 0);
952 assert_eq!(stats.isolated_layer_pixels.get(), 0);
953 assert_eq!(stats.top_isolated_layer_count.get(), 0);
954 }
955
956 #[test]
957 fn command_stats_accumulate_and_reset() {
958 let stats = FrameStats::default();
959
960 stats.record_command_stats(FrameCommandStats {
961 encoder_count: 2,
962 submit_count: 2,
963 pass_count: 5,
964 transient_texture_bytes: 1024,
965 retained_texture_bytes: 2048,
966 upload_bytes: 512,
967 });
968 stats.bump_shapes();
969
970 let snapshot = stats.snapshot();
971 assert_eq!(snapshot.submits, 2);
972 assert_eq!(snapshot.encoder_count, 2);
973 assert_eq!(snapshot.submit_count, 2);
974 assert_eq!(snapshot.pass_count, 5);
975 assert_eq!(snapshot.transient_texture_bytes, 1024);
976 assert_eq!(snapshot.retained_texture_bytes, 2048);
977 assert_eq!(snapshot.upload_bytes, 512);
978
979 stats.reset();
980 let reset = stats.snapshot();
981 assert_eq!(reset.submits, 0);
982 assert_eq!(reset.encoder_count, 0);
983 assert_eq!(reset.submit_count, 0);
984 assert_eq!(reset.pass_count, 0);
985 assert_eq!(reset.transient_texture_bytes, 0);
986 assert_eq!(reset.retained_texture_bytes, 0);
987 assert_eq!(reset.upload_bytes, 0);
988 }
989
990 #[test]
991 fn snapshot_adds_explicit_readback_command_stats() {
992 let stats = FrameStats::default();
993 stats.record_command_stats(FrameCommandStats {
994 encoder_count: 1,
995 submit_count: 1,
996 pass_count: 2,
997 transient_texture_bytes: 128,
998 retained_texture_bytes: 512,
999 upload_bytes: 64,
1000 });
1001 let snapshot = stats
1002 .snapshot()
1003 .with_command_stats_added(FrameCommandStats {
1004 encoder_count: 1,
1005 submit_count: 1,
1006 pass_count: 1,
1007 transient_texture_bytes: 0,
1008 retained_texture_bytes: 0,
1009 upload_bytes: 0,
1010 });
1011
1012 assert_eq!(snapshot.submits, 2);
1013 assert_eq!(snapshot.encoder_count, 2);
1014 assert_eq!(snapshot.submit_count, 2);
1015 assert_eq!(snapshot.pass_count, 3);
1016 assert_eq!(snapshot.transient_texture_bytes, 128);
1017 assert_eq!(snapshot.retained_texture_bytes, 512);
1018 assert_eq!(snapshot.upload_bytes, 64);
1019 }
1020
1021 #[test]
1022 fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
1023 let stats = FrameStats::default();
1024 let snapshot = stats.snapshot();
1025 let mut frame_count = 0;
1026
1027 stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
1028 assert_eq!(frame_count, 0);
1029
1030 stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
1031 assert_eq!(frame_count, 1);
1032 }
1033
1034 #[test]
1035 fn layer_surface_reasons_report_runtime_only_bits() {
1036 let reasons = LayerSurfaceReasons {
1037 immediate_shadow: true,
1038 text_local_surface: true,
1039 mixed_direct_content: true,
1040 ..LayerSurfaceReasons::default()
1041 };
1042
1043 assert!(reasons.has_any());
1044 assert!(reasons.has_renderer_forced_surface());
1045 assert_eq!(
1046 reasons.labels().collect::<Vec<_>>(),
1047 vec![
1048 "immediate_shadow",
1049 "text_local_surface",
1050 "mixed_direct_content"
1051 ]
1052 );
1053 assert_eq!(
1054 reasons.display(),
1055 "immediate_shadow+text_local_surface+mixed_direct_content"
1056 );
1057 }
1058
1059 #[test]
1060 fn immediate_shadow_only_is_diagnostic_not_isolating() {
1061 let reasons = LayerSurfaceReasons {
1062 immediate_shadow: true,
1063 ..LayerSurfaceReasons::default()
1064 };
1065
1066 assert!(!reasons.has_any());
1067 assert!(!reasons.has_renderer_forced_surface());
1068 assert_eq!(
1069 reasons.labels().collect::<Vec<_>>(),
1070 vec!["immediate_shadow"]
1071 );
1072 assert_eq!(reasons.display(), "immediate_shadow");
1073 }
1074
1075 #[test]
1076 fn mixed_direct_content_only_is_diagnostic_not_isolating() {
1077 let reasons = LayerSurfaceReasons {
1078 mixed_direct_content: true,
1079 ..LayerSurfaceReasons::default()
1080 };
1081
1082 assert!(!reasons.has_any());
1083 assert!(!reasons.has_renderer_forced_surface());
1084 assert_eq!(
1085 reasons.labels().collect::<Vec<_>>(),
1086 vec!["mixed_direct_content"]
1087 );
1088 assert_eq!(reasons.display(), "mixed_direct_content");
1089 }
1090
1091 #[test]
1092 fn has_any_matches_has_isolating_requirement_for_each_requirement() {
1093 let all_requirements = [
1094 SurfaceRequirement::ExplicitOffscreen,
1095 SurfaceRequirement::RenderEffect,
1096 SurfaceRequirement::Backdrop,
1097 SurfaceRequirement::GroupOpacity,
1098 SurfaceRequirement::BlendMode,
1099 SurfaceRequirement::ShapeClip,
1100 SurfaceRequirement::ImmediateShadow,
1101 SurfaceRequirement::TextMaterialMask,
1102 SurfaceRequirement::MotionStableCapture,
1103 SurfaceRequirement::NonTranslationTransform,
1104 SurfaceRequirement::MixedDirectContent,
1105 SurfaceRequirement::PixelStableComposite,
1106 ];
1107 for requirement in all_requirements {
1108 let set = SurfaceRequirementSet::default().with(requirement);
1109 let reasons = LayerSurfaceReasons::from(set);
1110 assert_eq!(
1111 reasons.has_any(),
1112 set.has_isolating_requirement(),
1113 "has_any vs has_isolating_requirement mismatch for {requirement:?}"
1114 );
1115 }
1116 }
1117
1118 #[test]
1119 fn top_isolated_layers_keep_largest_runtime_surfaces() {
1120 let stats = FrameStats::default();
1121 for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
1122 stats.record_isolated_layer_render(
1123 16 + index as u32,
1124 8 + index as u32,
1125 Some(index),
1126 Rect {
1127 x: index as f32,
1128 y: 0.0,
1129 width: 10.0,
1130 height: 10.0,
1131 },
1132 LayerSurfaceReasons {
1133 text_local_surface: true,
1134 ..LayerSurfaceReasons::default()
1135 },
1136 );
1137 }
1138
1139 let snapshot = stats.snapshot();
1140 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
1141 assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
1142 assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
1143 assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
1144 }
1145}