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
11macro_rules! stats_line {
14 ($($arg:tt)*) => {{
15 #[cfg(target_os = "android")]
16 log::info!($($arg)*);
17 #[cfg(not(target_os = "android"))]
18 eprintln!($($arg)*);
19 }};
20}
21
22const TOP_ISOLATED_LAYER_LIMIT: usize = 8;
23
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct IsolatedLayerStat {
26 pub node_id: Option<NodeId>,
27 pub logical_rect: Rect,
28 pub width: u32,
29 pub height: u32,
30}
31
32impl IsolatedLayerStat {
33 fn pixel_area(self) -> u64 {
34 (self.width as u64) * (self.height as u64)
35 }
36}
37
38impl Default for IsolatedLayerStat {
39 fn default() -> Self {
40 Self {
41 node_id: None,
42 logical_rect: Rect {
43 x: 0.0,
44 y: 0.0,
45 width: 0.0,
46 height: 0.0,
47 },
48 width: 0,
49 height: 0,
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq)]
55pub struct FrameStatsSnapshot {
56 pub submits: u32,
57 pub encoder_count: u32,
58 pub submit_count: u32,
59 pub pass_count: u32,
60 pub pass_pixels: u64,
63 pub copy_count: u32,
65 pub copy_pixels: u64,
66 pub offscreen_acquires: u32,
67 pub offscreen_news: u32,
68 pub offscreen_total_bytes: u64,
69 pub transient_texture_bytes: u64,
70 pub retained_texture_bytes: u64,
71 pub upload_bytes: u64,
72 pub upload_writes: u32,
75 pub isolated_layer_renders: u32,
76 pub isolated_layer_pixels: u64,
77 pub layer_cache_hits: u32,
78 pub capture_fixup_passes: u32,
80 pub layer_cache_misses: u32,
81 pub layer_cache_hit_pixels: u64,
82 pub layer_cache_miss_pixels: u64,
83 pub layer_cache_hits_by_kind: [u32; LAYER_RASTER_CACHE_KIND_COUNT],
85 pub layer_cache_misses_by_kind: [u32; LAYER_RASTER_CACHE_KIND_COUNT],
87 pub layer_cache_miss_pixels_by_kind: [u64; LAYER_RASTER_CACHE_KIND_COUNT],
89 pub shadow_shape_cache_hits: u32,
90 pub shadow_shape_cache_misses: u32,
91 pub shadow_shape_cache_hit_pixels: u64,
92 pub shadow_shape_cache_miss_pixels: u64,
93 pub shadow_fully_occluded_composites: u32,
96 pub shadow_text_blur_fallbacks: u32,
97 pub blur_passes: u32,
98 pub stages: u32,
101 pub backdrop_admissions: u32,
104 pub prefix_admissions: u32,
106 pub substrates: u32,
109 pub composite_passes: u32,
113 pub effect_applies: u32,
114 pub shader_pixels: u64,
117 pub glass_rasterized_pixels: u64,
118 pub blur_pixels: u64,
122 pub shape_fill_pixels: u64,
126 pub shape_fill_pixels_by_class: [u64; ShapeFill::CLASSES],
129 pub shape_vertices: u64,
133 pub shape_passes: u32,
134 pub shape_pipeline_fallback_draws: u32,
136 pub shape_specialized_draws: u32,
138 pub shape_interior_draws: u32,
140 pub pipelines_created: u64,
144 pub shader_pipeline_fallback_draws: u32,
147 pub shader_specialized_draws: u32,
149 pub image_passes: u32,
150 pub text_passes: u32,
151 pub draw_calls: u32,
158 pub text_image_cache_hits: u32,
159 pub text_image_cache_misses: u32,
160 pub text_image_cache_hit_pixels: u64,
161 pub text_image_cache_miss_pixels: u64,
162 pub text_image_raster_bytes: u64,
163 pub text_glyph_atlas_hits: u32,
164 pub text_glyph_atlas_misses: u32,
165 pub text_glyph_atlas_miss_pixels: u64,
166 pub offscreen_pool_size: u32,
167 pub offscreen_pool_bytes: u64,
168 pub text_pool_size: u32,
169 pub layer_cache_size: u32,
170 pub layer_cache_bytes: u64,
171 pub image_cache_size: u32,
172 pub text_cache_size: u32,
173 pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
174 pub top_isolated_layer_count: usize,
175}
176
177impl FrameStatsSnapshot {
178 fn shape_fill_by_class_text(&self) -> String {
179 let mut text = String::new();
180 for (label, pixels) in ShapeFill::LABELS
181 .iter()
182 .zip(self.shape_fill_pixels_by_class)
183 {
184 if pixels > 0 {
185 use std::fmt::Write;
186 let _ = write!(text, " {label}={:.2}MP", pixels as f64 / 1_000_000.0);
187 }
188 }
189 text
190 }
191
192 pub(crate) fn with_command_stats_added(mut self, stats: FrameCommandStats) -> Self {
193 self.submits = self.submits.saturating_add(stats.submit_count);
194 self.encoder_count = self.encoder_count.saturating_add(stats.encoder_count);
195 self.submit_count = self.submit_count.saturating_add(stats.submit_count);
196 self.pass_count = self.pass_count.saturating_add(stats.pass_count);
197 self.pass_pixels = self.pass_pixels.saturating_add(stats.pass_pixels);
198 self.copy_count = self.copy_count.saturating_add(stats.copy_count);
199 self.copy_pixels = self.copy_pixels.saturating_add(stats.copy_pixels);
200 self.transient_texture_bytes = self
201 .transient_texture_bytes
202 .saturating_add(stats.transient_texture_bytes);
203 self.retained_texture_bytes = self
204 .retained_texture_bytes
205 .max(stats.retained_texture_bytes);
206 self.upload_bytes = self.upload_bytes.saturating_add(stats.upload_bytes);
207 self.upload_writes = self.upload_writes.saturating_add(stats.upload_writes);
208 self.offscreen_acquires = self
209 .offscreen_acquires
210 .saturating_add(stats.transient_acquires);
211 self.offscreen_news = self.offscreen_news.saturating_add(stats.transient_news);
212 self
213 }
214
215 fn top_isolated_display(&self) -> String {
218 match &self.top_isolated_layers[0] {
219 Some(top) => format!(
220 "{}x{}@({:.0},{:.0})",
221 top.width, top.height, top.logical_rect.x, top.logical_rect.y
222 ),
223 None => "-".to_string(),
224 }
225 }
226
227 pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
228 self.top_isolated_layers
229 .into_iter()
230 .flatten()
231 .take(self.top_isolated_layer_count)
232 }
233
234 fn layer_cache_hit_rate(self) -> f64 {
235 let total = self.layer_cache_hits + self.layer_cache_misses;
236 if total > 0 {
237 (self.layer_cache_hits as f64 / total as f64) * 100.0
238 } else {
239 0.0
240 }
241 }
242
243 fn miss_pixels_by_kind_display(&self) -> String {
244 by_kind_display(&self.layer_cache_miss_pixels_by_kind, |pixels| {
245 format!("{:.2}MP", pixels as f64 / 1_000_000.0)
246 })
247 }
248
249 fn hits_by_kind_display(&self) -> String {
250 by_kind_display(&self.layer_cache_hits_by_kind, |hits| hits.to_string())
251 }
252
253 fn print(self, frame_count: u64) {
254 let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
255 let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
256 let retained_mb = self.retained_texture_bytes as f64 / (1024.0 * 1024.0);
257 let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
258 let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
259 let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
260 let shadow_cache_hit_mpx = self.shadow_shape_cache_hit_pixels as f64 / 1_000_000.0;
261 let shadow_cache_miss_mpx = self.shadow_shape_cache_miss_pixels as f64 / 1_000_000.0;
262 let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
263 let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
264 stats_line!(
265 "[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 | \
266 uploads={:.2}MB writes={} | \
267 isolated_layers={} area={:.2}MP top={} | \
268 layer_cache: hit={} miss={} {:.1}% hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) hit_by_kind={} miss_px_by_kind={} | \
269 shadow_cache: shape_hit={} shape_miss={} hit_px={:.2}MP miss_px={:.2}MP text_blur_fallback={} | \
270 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={} interiors={} image={} text={} draws={} | \
271 text_img_cache: hit={} miss={} hit_px={:.2}MP miss_px={:.2}MP raster={:.2}MB | \
272 text_glyph_atlas: hit={} miss={} miss_px={:.2}MP | \
273 caches: text_pool={} img={} txt={}",
274 frame_count,
275 self.encoder_count,
276 self.submit_count,
277 self.pass_count,
278 self.pass_pixels as f64 / 1_000_000.0,
279 self.copy_count,
280 self.copy_pixels as f64 / 1_000_000.0,
281 self.offscreen_acquires,
282 self.offscreen_news,
283 mb,
284 self.offscreen_pool_size,
285 pool_mb,
286 retained_mb,
287 upload_mb,
288 self.upload_writes,
289 self.isolated_layer_renders,
290 isolated_layer_mpx,
291 self.top_isolated_display(),
292 self.layer_cache_hits,
293 self.layer_cache_misses,
294 self.layer_cache_hit_rate(),
295 layer_cache_hit_mpx,
296 layer_cache_miss_mpx,
297 self.layer_cache_size,
298 layer_cache_mb,
299 self.hits_by_kind_display(),
300 self.miss_pixels_by_kind_display(),
301 self.shadow_shape_cache_hits,
302 self.shadow_shape_cache_misses,
303 shadow_cache_hit_mpx,
304 shadow_cache_miss_mpx,
305 self.shadow_text_blur_fallbacks,
306 self.stages,
307 self.backdrop_admissions,
308 self.blur_passes,
309 self.substrates,
310 self.composite_passes,
311 self.effect_applies,
312 self.shader_pixels as f64 / 1_000_000.0,
313 self.glass_rasterized_pixels as f64 / 1_000_000.0,
314 self.blur_pixels as f64 / 1_000_000.0,
315 self.shape_passes,
316 self.shape_fill_pixels as f64 / 1_000_000.0,
317 self.shape_fill_by_class_text(),
318 self.shape_vertices,
319 self.shape_interior_draws,
320 self.image_passes,
321 self.text_passes,
322 self.draw_calls,
323 self.text_image_cache_hits,
324 self.text_image_cache_misses,
325 self.text_image_cache_hit_pixels as f64 / 1_000_000.0,
326 self.text_image_cache_miss_pixels as f64 / 1_000_000.0,
327 self.text_image_raster_bytes as f64 / (1024.0 * 1024.0),
328 self.text_glyph_atlas_hits,
329 self.text_glyph_atlas_misses,
330 self.text_glyph_atlas_miss_pixels as f64 / 1_000_000.0,
331 self.text_pool_size,
332 self.image_cache_size,
333 self.text_cache_size,
334 );
335 for (index, layer) in self.top_isolated_layers().enumerate() {
336 stats_line!(
337 " [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{}",
338 layer.node_id,
339 layer.logical_rect.x,
340 layer.logical_rect.y,
341 layer.logical_rect.width,
342 layer.logical_rect.height,
343 layer.width,
344 layer.height,
345 );
346 }
347 }
348}
349
350#[derive(Default)]
351pub(crate) struct FrameStats {
352 pub submits: Cell<u32>,
353 pub command_encoder_count: Cell<u32>,
354 pub command_submit_count: Cell<u32>,
355 pub command_pass_count: Cell<u32>,
356 pub command_pass_pixels: Cell<u64>,
357 pub command_copy_count: Cell<u32>,
358 pub command_copy_pixels: Cell<u64>,
359 pub command_transient_texture_bytes: Cell<u64>,
360 pub command_retained_texture_bytes: Cell<u64>,
361 pub command_upload_bytes: Cell<u64>,
362 pub offscreen_acquires: Cell<u32>,
363 pub offscreen_news: Cell<u32>,
364 pub offscreen_total_bytes: Cell<u64>,
365 pub upload_writes: Cell<u32>,
366 pub isolated_layer_renders: Cell<u32>,
367 pub isolated_layer_pixels: Cell<u64>,
368 pub layer_cache_hits: Cell<u32>,
369 pub capture_fixup_passes: Cell<u32>,
370 pub layer_cache_misses: Cell<u32>,
371 pub layer_cache_hit_pixels: Cell<u64>,
372 pub layer_cache_miss_pixels: Cell<u64>,
373 pub layer_cache_hits_by_kind: [Cell<u32>; LAYER_RASTER_CACHE_KIND_COUNT],
374 pub layer_cache_misses_by_kind: [Cell<u32>; LAYER_RASTER_CACHE_KIND_COUNT],
375 pub layer_cache_miss_pixels_by_kind: [Cell<u64>; LAYER_RASTER_CACHE_KIND_COUNT],
376 pub shadow_shape_cache_hits: Cell<u32>,
377 pub shadow_shape_cache_misses: Cell<u32>,
378 pub shadow_shape_cache_hit_pixels: Cell<u64>,
379 pub shadow_shape_cache_miss_pixels: Cell<u64>,
380 pub shadow_fully_occluded_composites: Cell<u32>,
381 pub shadow_text_blur_fallbacks: Cell<u32>,
382 pub blur_passes: Cell<u32>,
383 pub stages: Cell<u32>,
384 pub backdrop_admissions: Cell<u32>,
385 pub prefix_admissions: Cell<u32>,
386 pub substrates: Cell<u32>,
387 pub composite_passes: Cell<u32>,
388 pub effect_applies: Cell<u32>,
389 pub shader_pixels: Cell<u64>,
390 pub glass_rasterized_pixels: Cell<u64>,
391 pub blur_pixels: Cell<u64>,
392 pub shape_fill_pixels: Cell<u64>,
393 pub shape_fill_pixels_by_class: Cell<[u64; ShapeFill::CLASSES]>,
394 pub shape_vertices: Cell<u64>,
395 pub shape_passes: Cell<u32>,
396 pub shape_pipeline_fallback_draws: Cell<u32>,
397 pub shape_specialized_draws: Cell<u32>,
398 pub shape_interior_draws: Cell<u32>,
399 pub shader_pipeline_fallback_draws: Cell<u32>,
400 pub shader_specialized_draws: Cell<u32>,
401 pub image_passes: Cell<u32>,
402 pub text_passes: Cell<u32>,
403 pub draw_calls: Cell<u32>,
404 pub text_image_cache_hits: Cell<u32>,
405 pub text_image_cache_misses: Cell<u32>,
406 pub text_image_cache_hit_pixels: Cell<u64>,
407 pub text_image_cache_miss_pixels: Cell<u64>,
408 pub text_image_raster_bytes: Cell<u64>,
409 pub text_glyph_atlas_hits: Cell<u32>,
410 pub text_glyph_atlas_misses: Cell<u32>,
411 pub text_glyph_atlas_miss_pixels: Cell<u64>,
412 pub offscreen_pool_size: Cell<u32>,
413 pub offscreen_pool_bytes: Cell<u64>,
414 pub text_pool_size: Cell<u32>,
415 pub layer_cache_size: Cell<u32>,
416 pub layer_cache_bytes: Cell<u64>,
417 pub image_cache_size: Cell<u32>,
418 pub text_cache_size: Cell<u32>,
419 top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
420 top_isolated_layer_count: Cell<usize>,
421 shadow_shape_cache_miss_log_count: Cell<u32>,
422}
423
424impl FrameStats {
425 pub fn record_capture_fixup_pass(&self) {
426 self.capture_fixup_passes
427 .set(self.capture_fixup_passes.get().saturating_add(1));
428 }
429
430 pub fn record_stages(&self, count: u32) {
431 self.stages.set(self.stages.get().saturating_add(count));
432 }
433
434 pub fn record_backdrop_admission(&self) {
435 self.backdrop_admissions
436 .set(self.backdrop_admissions.get().saturating_add(1));
437 }
438
439 pub fn record_prefix_admission(&self) {
440 self.prefix_admissions
441 .set(self.prefix_admissions.get().saturating_add(1));
442 }
443
444 pub fn record_command_stats(&self, stats: FrameCommandStats) {
445 self.submits
446 .set(self.submits.get().saturating_add(stats.submit_count));
447 self.command_encoder_count.set(
448 self.command_encoder_count
449 .get()
450 .saturating_add(stats.encoder_count),
451 );
452 self.command_submit_count.set(
453 self.command_submit_count
454 .get()
455 .saturating_add(stats.submit_count),
456 );
457 self.command_pass_count.set(
458 self.command_pass_count
459 .get()
460 .saturating_add(stats.pass_count),
461 );
462 self.command_pass_pixels.set(
463 self.command_pass_pixels
464 .get()
465 .saturating_add(stats.pass_pixels),
466 );
467 self.command_copy_count.set(
468 self.command_copy_count
469 .get()
470 .saturating_add(stats.copy_count),
471 );
472 self.command_copy_pixels.set(
473 self.command_copy_pixels
474 .get()
475 .saturating_add(stats.copy_pixels),
476 );
477 self.command_transient_texture_bytes.set(
478 self.command_transient_texture_bytes
479 .get()
480 .saturating_add(stats.transient_texture_bytes),
481 );
482 self.command_retained_texture_bytes.set(
483 self.command_retained_texture_bytes
484 .get()
485 .max(stats.retained_texture_bytes),
486 );
487 self.command_upload_bytes.set(
488 self.command_upload_bytes
489 .get()
490 .saturating_add(stats.upload_bytes),
491 );
492 self.upload_writes
493 .set(self.upload_writes.get().saturating_add(stats.upload_writes));
494 self.offscreen_acquires.set(
495 self.offscreen_acquires
496 .get()
497 .saturating_add(stats.transient_acquires),
498 );
499 self.offscreen_news.set(
500 self.offscreen_news
501 .get()
502 .saturating_add(stats.transient_news),
503 );
504 }
505
506 pub fn record_offscreen_acquire(
507 &self,
508 width: u32,
509 height: u32,
510 format: wgpu::TextureFormat,
511 is_new: bool,
512 ) {
513 self.offscreen_acquires
514 .set(self.offscreen_acquires.get() + 1);
515 if is_new {
516 self.offscreen_news.set(self.offscreen_news.get() + 1);
517 }
518 self.offscreen_total_bytes.set(
519 self.offscreen_total_bytes.get()
520 + (width as u64)
521 * (height as u64)
522 * crate::frame_graph::texture_format_bytes_per_pixel(format),
523 );
524 }
525
526 pub fn record_isolated_layer_render(
527 &self,
528 width: u32,
529 height: u32,
530 node_id: Option<NodeId>,
531 logical_rect: Rect,
532 ) {
533 self.isolated_layer_renders
534 .set(self.isolated_layer_renders.get().saturating_add(1));
535 self.isolated_layer_pixels.set(
536 self.isolated_layer_pixels
537 .get()
538 .saturating_add((width as u64) * (height as u64)),
539 );
540 self.record_top_isolated_layer(IsolatedLayerStat {
541 node_id,
542 logical_rect,
543 width,
544 height,
545 });
546 }
547
548 pub fn record_layer_cache_hit(&self, key: &LayerRasterCacheKey, width: u32, height: u32) {
549 let by_kind = &self.layer_cache_hits_by_kind[key.kind_slot()];
550 by_kind.set(by_kind.get().saturating_add(1));
551 self.layer_cache_hits
552 .set(self.layer_cache_hits.get().saturating_add(1));
553 self.layer_cache_hit_pixels.set(
554 self.layer_cache_hit_pixels
555 .get()
556 .saturating_add((width as u64) * (height as u64)),
557 );
558 }
559
560 pub fn record_layer_cache_miss(&self, key: &LayerRasterCacheKey, width: u32, height: u32) {
561 let slot = key.kind_slot();
562 let by_kind = &self.layer_cache_misses_by_kind[slot];
563 by_kind.set(by_kind.get().saturating_add(1));
564 let pixels_by_kind = &self.layer_cache_miss_pixels_by_kind[slot];
565 pixels_by_kind.set(
566 pixels_by_kind
567 .get()
568 .saturating_add((width as u64) * (height as u64)),
569 );
570 self.layer_cache_misses
571 .set(self.layer_cache_misses.get().saturating_add(1));
572 self.layer_cache_miss_pixels.set(
573 self.layer_cache_miss_pixels
574 .get()
575 .saturating_add((width as u64) * (height as u64)),
576 );
577 }
578
579 pub fn record_shadow_shape_cache_hit(&self, composited_pixels: u64) {
580 self.shadow_shape_cache_hits
581 .set(self.shadow_shape_cache_hits.get().saturating_add(1));
582 self.shadow_shape_cache_hit_pixels.set(
583 self.shadow_shape_cache_hit_pixels
584 .get()
585 .saturating_add(composited_pixels),
586 );
587 }
588
589 pub fn record_shadow_fully_occluded(&self) {
590 self.shadow_fully_occluded_composites.set(
591 self.shadow_fully_occluded_composites
592 .get()
593 .saturating_add(1),
594 );
595 }
596
597 pub fn record_shadow_shape_cache_miss(&self, width: u32, height: u32) {
598 self.shadow_shape_cache_misses
599 .set(self.shadow_shape_cache_misses.get().saturating_add(1));
600 self.shadow_shape_cache_miss_pixels.set(
601 self.shadow_shape_cache_miss_pixels
602 .get()
603 .saturating_add((width as u64) * (height as u64)),
604 );
605 }
606
607 #[expect(clippy::too_many_arguments)]
608 pub fn maybe_print_shadow_shape_cache_miss(
609 &self,
610 width: u32,
611 height: u32,
612 content_hash: u64,
613 blur_radius: f32,
614 viewport_offset: [f32; 2],
615 shape_count: usize,
616 clip: Option<Rect>,
617 ) {
618 if !shadow_cache_diagnostics_enabled() {
619 return;
620 }
621
622 let count = self.shadow_shape_cache_miss_log_count.get();
623 if count >= 16 {
624 return;
625 }
626 self.shadow_shape_cache_miss_log_count.set(count + 1);
627
628 let clip_text = clip.map_or_else(
629 || "none".to_string(),
630 |clip| {
631 format!(
632 "({:.1},{:.1},{:.1},{:.1})",
633 clip.x, clip.y, clip.width, clip.height
634 )
635 },
636 );
637 stats_line!(
638 "[shadow-cache-miss #{count}] size={}x{} content_hash={content_hash} blur={:.2} viewport_offset=({:.1},{:.1}) shapes={} clip={}",
639 width,
640 height,
641 blur_radius,
642 viewport_offset[0],
643 viewport_offset[1],
644 shape_count,
645 clip_text,
646 );
647 }
648
649 pub fn record_shadow_text_blur_fallback(&self) {
650 self.shadow_text_blur_fallbacks
651 .set(self.shadow_text_blur_fallbacks.get().saturating_add(1));
652 }
653
654 pub fn add_shape_fill(&self, fill: ShapeFill) {
655 self.shape_fill_pixels.set(
656 self.shape_fill_pixels
657 .get()
658 .saturating_add(fill.total().round() as u64),
659 );
660 let mut by_class = self.shape_fill_pixels_by_class.get();
661 for (total, pixels) in by_class.iter_mut().zip(fill.pixels) {
662 *total = total.saturating_add(pixels.round() as u64);
663 }
664 self.shape_fill_pixels_by_class.set(by_class);
665 self.shape_vertices
666 .set(self.shape_vertices.get().saturating_add(fill.vertices));
667 }
668
669 pub fn bump_shapes(&self) {
670 self.shape_passes.set(self.shape_passes.get() + 1);
671 }
672
673 pub fn bump_images(&self) {
674 self.image_passes.set(self.image_passes.get() + 1);
675 }
676
677 pub fn add_draw_calls(&self, count: u32) {
678 self.draw_calls
679 .set(self.draw_calls.get().saturating_add(count));
680 }
681
682 pub fn bump_text(&self) {
683 self.text_passes.set(self.text_passes.get() + 1);
684 }
685
686 pub fn record_text_image_cache_hit(&self, width: u32, height: u32) {
687 self.text_image_cache_hits
688 .set(self.text_image_cache_hits.get().saturating_add(1));
689 self.text_image_cache_hit_pixels.set(
690 self.text_image_cache_hit_pixels
691 .get()
692 .saturating_add((width as u64) * (height as u64)),
693 );
694 }
695
696 pub fn record_text_image_cache_miss(&self, width: u32, height: u32) {
697 let pixels = (width as u64) * (height as u64);
698 self.text_image_cache_misses
699 .set(self.text_image_cache_misses.get().saturating_add(1));
700 self.text_image_cache_miss_pixels.set(
701 self.text_image_cache_miss_pixels
702 .get()
703 .saturating_add(pixels),
704 );
705 self.text_image_raster_bytes.set(
706 self.text_image_raster_bytes
707 .get()
708 .saturating_add(pixels * 4),
709 );
710 }
711
712 pub fn record_text_glyph_atlas_hits(&self, count: u32) {
713 self.text_glyph_atlas_hits
714 .set(self.text_glyph_atlas_hits.get().saturating_add(count));
715 }
716
717 pub fn record_text_glyph_atlas_miss(&self, width: u32, height: u32) {
718 self.text_glyph_atlas_misses
719 .set(self.text_glyph_atlas_misses.get().saturating_add(1));
720 self.text_glyph_atlas_miss_pixels.set(
721 self.text_glyph_atlas_miss_pixels
722 .get()
723 .saturating_add((width as u64) * (height as u64)),
724 );
725 }
726
727 pub fn snapshot(&self) -> FrameStatsSnapshot {
728 let retained_texture_bytes = self
729 .offscreen_pool_bytes
730 .get()
731 .saturating_add(self.layer_cache_bytes.get());
732 FrameStatsSnapshot {
733 submits: self.submits.get(),
734 encoder_count: self.command_encoder_count.get(),
735 submit_count: self.command_submit_count.get(),
736 pass_count: self.command_pass_count.get(),
737 pass_pixels: self.command_pass_pixels.get(),
738 copy_count: self.command_copy_count.get(),
739 copy_pixels: self.command_copy_pixels.get(),
740 offscreen_acquires: self.offscreen_acquires.get(),
741 offscreen_news: self.offscreen_news.get(),
742 offscreen_total_bytes: self.offscreen_total_bytes.get(),
743 transient_texture_bytes: self
744 .offscreen_total_bytes
745 .get()
746 .saturating_add(self.command_transient_texture_bytes.get()),
747 retained_texture_bytes: retained_texture_bytes
748 .saturating_add(self.command_retained_texture_bytes.get()),
749 upload_bytes: self.command_upload_bytes.get(),
750 upload_writes: self.upload_writes.get(),
751 isolated_layer_renders: self.isolated_layer_renders.get(),
752 isolated_layer_pixels: self.isolated_layer_pixels.get(),
753 layer_cache_hits: self.layer_cache_hits.get(),
754 capture_fixup_passes: self.capture_fixup_passes.get(),
755 layer_cache_misses: self.layer_cache_misses.get(),
756 layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
757 layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
758 layer_cache_hits_by_kind: self.layer_cache_hits_by_kind.each_ref().map(Cell::get),
759 layer_cache_misses_by_kind: self.layer_cache_misses_by_kind.each_ref().map(Cell::get),
760 layer_cache_miss_pixels_by_kind: self
761 .layer_cache_miss_pixels_by_kind
762 .each_ref()
763 .map(Cell::get),
764 shadow_shape_cache_hits: self.shadow_shape_cache_hits.get(),
765 shadow_shape_cache_misses: self.shadow_shape_cache_misses.get(),
766 shadow_shape_cache_hit_pixels: self.shadow_shape_cache_hit_pixels.get(),
767 shadow_shape_cache_miss_pixels: self.shadow_shape_cache_miss_pixels.get(),
768 shadow_fully_occluded_composites: self.shadow_fully_occluded_composites.get(),
769 shadow_text_blur_fallbacks: self.shadow_text_blur_fallbacks.get(),
770 blur_passes: self.blur_passes.get(),
771 stages: self.stages.get(),
772 backdrop_admissions: self.backdrop_admissions.get(),
773 prefix_admissions: self.prefix_admissions.get(),
774 substrates: self.substrates.get(),
775 composite_passes: self.composite_passes.get(),
776 effect_applies: self.effect_applies.get(),
777 shader_pixels: self.shader_pixels.get(),
778 glass_rasterized_pixels: self.glass_rasterized_pixels.get(),
779 blur_pixels: self.blur_pixels.get(),
780 shape_fill_pixels: self.shape_fill_pixels.get(),
781 shape_fill_pixels_by_class: self.shape_fill_pixels_by_class.get(),
782 shape_vertices: self.shape_vertices.get(),
783 shape_passes: self.shape_passes.get(),
784 shape_pipeline_fallback_draws: self.shape_pipeline_fallback_draws.get(),
785 shape_specialized_draws: self.shape_specialized_draws.get(),
786 shape_interior_draws: self.shape_interior_draws.get(),
787 pipelines_created: crate::render::pipelines_created(),
788 shader_pipeline_fallback_draws: self.shader_pipeline_fallback_draws.get(),
789 shader_specialized_draws: self.shader_specialized_draws.get(),
790 image_passes: self.image_passes.get(),
791 text_passes: self.text_passes.get(),
792 draw_calls: self.draw_calls.get(),
793 text_image_cache_hits: self.text_image_cache_hits.get(),
794 text_image_cache_misses: self.text_image_cache_misses.get(),
795 text_image_cache_hit_pixels: self.text_image_cache_hit_pixels.get(),
796 text_image_cache_miss_pixels: self.text_image_cache_miss_pixels.get(),
797 text_image_raster_bytes: self.text_image_raster_bytes.get(),
798 text_glyph_atlas_hits: self.text_glyph_atlas_hits.get(),
799 text_glyph_atlas_misses: self.text_glyph_atlas_misses.get(),
800 text_glyph_atlas_miss_pixels: self.text_glyph_atlas_miss_pixels.get(),
801 offscreen_pool_size: self.offscreen_pool_size.get(),
802 offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
803 text_pool_size: self.text_pool_size.get(),
804 layer_cache_size: self.layer_cache_size.get(),
805 layer_cache_bytes: self.layer_cache_bytes.get(),
806 image_cache_size: self.image_cache_size.get(),
807 text_cache_size: self.text_cache_size.get(),
808 top_isolated_layers: *self.top_isolated_layers.borrow(),
809 top_isolated_layer_count: self.top_isolated_layer_count.get(),
810 }
811 }
812
813 pub fn reset(&self) {
814 self.submits.set(0);
815 self.command_encoder_count.set(0);
816 self.command_submit_count.set(0);
817 self.command_pass_count.set(0);
818 self.command_pass_pixels.set(0);
819 self.command_copy_count.set(0);
820 self.command_copy_pixels.set(0);
821 self.command_transient_texture_bytes.set(0);
822 self.command_retained_texture_bytes.set(0);
823 self.command_upload_bytes.set(0);
824 self.upload_writes.set(0);
825 self.offscreen_acquires.set(0);
826 self.offscreen_news.set(0);
827 self.offscreen_total_bytes.set(0);
828 self.isolated_layer_renders.set(0);
829 self.isolated_layer_pixels.set(0);
830 self.layer_cache_hits.set(0);
831 self.capture_fixup_passes.set(0);
832 self.layer_cache_misses.set(0);
833 self.layer_cache_hit_pixels.set(0);
834 self.layer_cache_miss_pixels.set(0);
835 for slot in 0..LAYER_RASTER_CACHE_KIND_COUNT {
836 self.layer_cache_hits_by_kind[slot].set(0);
837 self.layer_cache_misses_by_kind[slot].set(0);
838 self.layer_cache_miss_pixels_by_kind[slot].set(0);
839 }
840 self.shadow_shape_cache_hits.set(0);
841 self.shadow_shape_cache_misses.set(0);
842 self.shadow_shape_cache_hit_pixels.set(0);
843 self.shadow_shape_cache_miss_pixels.set(0);
844 self.shadow_fully_occluded_composites.set(0);
845 self.shadow_text_blur_fallbacks.set(0);
846 self.blur_passes.set(0);
847 self.stages.set(0);
848 self.backdrop_admissions.set(0);
849 self.prefix_admissions.set(0);
850 self.substrates.set(0);
851 self.composite_passes.set(0);
852 self.effect_applies.set(0);
853 self.shader_pixels.set(0);
854 self.glass_rasterized_pixels.set(0);
855 self.blur_pixels.set(0);
856 self.shape_fill_pixels.set(0);
857 self.shape_fill_pixels_by_class.set([0; ShapeFill::CLASSES]);
858 self.shape_vertices.set(0);
859 self.shape_passes.set(0);
860 self.shape_pipeline_fallback_draws.set(0);
861 self.shape_specialized_draws.set(0);
862 self.shape_interior_draws.set(0);
863 self.shader_pipeline_fallback_draws.set(0);
864 self.shader_specialized_draws.set(0);
865 self.image_passes.set(0);
866 self.text_passes.set(0);
867 self.draw_calls.set(0);
868 self.text_image_cache_hits.set(0);
869 self.text_image_cache_misses.set(0);
870 self.text_image_cache_hit_pixels.set(0);
871 self.text_image_cache_miss_pixels.set(0);
872 self.text_image_raster_bytes.set(0);
873 self.text_glyph_atlas_hits.set(0);
874 self.text_glyph_atlas_misses.set(0);
875 self.text_glyph_atlas_miss_pixels.set(0);
876 *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
877 self.top_isolated_layer_count.set(0);
878 self.shadow_shape_cache_miss_log_count.set(0);
879 }
880
881 pub fn maybe_print_snapshot(
882 &self,
883 snapshot: FrameStatsSnapshot,
884 frame_count: &mut u64,
885 enabled: bool,
886 ) {
887 if !enabled {
888 return;
889 }
890 *frame_count += 1;
891 if (*frame_count).is_multiple_of(60) {
892 snapshot.print(*frame_count);
893 }
894 }
895
896 fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
897 let mut top_layers = self.top_isolated_layers.borrow_mut();
898 let len = self.top_isolated_layer_count.get();
899 let insert_at = top_layers[..len]
900 .iter()
901 .enumerate()
902 .find_map(|(index, existing)| {
903 existing
904 .filter(|existing| layer.pixel_area() > existing.pixel_area())
905 .map(|_| index)
906 })
907 .unwrap_or(len);
908
909 if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
910 return;
911 }
912
913 let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
914 len + 1
915 } else {
916 TOP_ISOLATED_LAYER_LIMIT
917 };
918
919 let mut index = new_len.saturating_sub(1);
920 while index > insert_at {
921 top_layers[index] = top_layers[index - 1];
922 index -= 1;
923 }
924 top_layers[insert_at] = Some(layer);
925 self.top_isolated_layer_count.set(new_len);
926 }
927}
928
929static GPU_STATS: DebugToggle = DebugToggle::new("CRANPOSE_GPU_STATS");
930
931pub(crate) fn gpu_stats_enabled() -> bool {
932 GPU_STATS.flag()
933}
934
935pub(crate) fn print_gpu_memory_report(device: &wgpu::Device, frame_count: u64) {
936 let Some(report) = device.generate_allocator_report() else {
937 return;
938 };
939
940 const MB: f64 = 1024.0 * 1024.0;
941 let mut blocks = String::new();
942 for block in &report.blocks {
943 if !blocks.is_empty() {
944 blocks.push('+');
945 }
946 blocks.push_str(&format!("{:.1}", block.size as f64 / MB));
947 }
948
949 stats_line!(
950 "[GPU-MEM f#{}] reserved={:.1}MB allocated={:.1}MB blocks={}[{}MB] allocations={} | largest={:.6?}",
951 frame_count,
952 report.total_reserved_bytes as f64 / MB,
953 report.total_allocated_bytes as f64 / MB,
954 report.blocks.len(),
955 blocks,
956 report.allocations.len(),
957 report,
958 );
959}
960
961static SHADOW_CACHE_DIAG: DebugToggle = DebugToggle::new("CRANPOSE_GPU_SHADOW_CACHE_DIAG");
962
963fn shadow_cache_diagnostics_enabled() -> bool {
964 SHADOW_CACHE_DIAG.flag()
965}
966
967fn by_kind_display<T: Copy + Default + PartialEq>(
968 by_kind: &[T; LAYER_RASTER_CACHE_KIND_COUNT],
969 format: impl Fn(T) -> String,
970) -> String {
971 LAYER_RASTER_CACHE_KIND_LABELS
972 .iter()
973 .zip(by_kind)
974 .filter(|(_, value)| **value != T::default())
975 .map(|(label, value)| format!("{label}={}", format(*value)))
976 .collect::<Vec<_>>()
977 .join(",")
978}
979
980#[cfg(test)]
981#[path = "tests/gpu_stats_tests.rs"]
982mod tests;