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