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