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