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