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