Skip to main content

cranpose_render_wgpu/
gpu_stats.rs

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
11/// A diagnostics line: logcat on Android, where nothing reads stderr, and
12/// stderr elsewhere.
13macro_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    /// Sum of every render pass's first color target area: the tile traffic a
61    /// tiling GPU pays whatever the passes draw.
62    pub pass_pixels: u64,
63    /// Texture regions copied outside any pass, and their texels.
64    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    /// Buffer writes the frame staged: uploads that reach the GPU as one
73    /// write each, however many draws they carry.
74    pub upload_writes: u32,
75    pub isolated_layer_renders: u32,
76    pub isolated_layer_pixels: u64,
77    pub layer_cache_hits: u32,
78    /// Passes that drew fix-ups over copied capture regions.
79    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    /// Hits per key kind, indexed by `LayerRasterCacheKey::kind_slot`.
84    pub layer_cache_hits_by_kind: [u32; LAYER_RASTER_CACHE_KIND_COUNT],
85    /// Stores per key kind, indexed by `LayerRasterCacheKey::kind_slot`.
86    pub layer_cache_misses_by_kind: [u32; LAYER_RASTER_CACHE_KIND_COUNT],
87    /// Stored pixels per key kind, indexed by `LayerRasterCacheKey::kind_slot`.
88    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    /// Converted shadows whose caster occluded every visible coverage pixel
94    /// this frame: they composite nothing and their draw item is dropped.
95    pub shadow_fully_occluded_composites: u32,
96    pub shadow_text_blur_fallbacks: u32,
97    pub blur_passes: u32,
98    /// Backdrop stages resolved: every stage flushes the page and captures
99    /// once, so the layer pass count is one more than this.
100    pub stages: u32,
101    /// Backdrops whose stage texture the layer cache pinned this frame, so
102    /// later frames replay their composites from it.
103    pub backdrop_admissions: u32,
104    /// Opaque page prefixes copied into the layer cache this frame.
105    pub prefix_admissions: u32,
106    /// Captures averaged into a quarter-size substrate for a shader's wide
107    /// taps.
108    pub substrates: u32,
109    /// Passes that draw at least one resolved composite: a final pass holding
110    /// twelve glass tails counts once, a composite resolved into its own
111    /// texture counts its own pass.
112    pub composite_passes: u32,
113    pub effect_applies: u32,
114    /// Pixels shaded by runtime shader composites: what every glass surface
115    /// costs a tiling GPU per frame, each capture that re-shades one included.
116    pub shader_pixels: u64,
117    pub glass_rasterized_pixels: u64,
118    /// Pixels the blur passes write: every downsample, horizontal and
119    /// vertical pass of a backdrop's blur and of its substrates, within
120    /// the scissor the pass was given.
121    pub blur_pixels: u64,
122    /// Pixels the shape draws rasterize: a band drawn as a mesh counts its
123    /// mesh, everything else its quad. Fill rate is what a scene of rings
124    /// costs a tiling GPU, however few draws it takes.
125    pub shape_fill_pixels: u64,
126    /// `shape_fill_pixels` split by shape kind and brush, in the order of
127    /// `ShapeFill::LABELS`: which records a fill-bound frame pays for.
128    pub shape_fill_pixels_by_class: [u64; ShapeFill::CLASSES],
129    /// Vertices the shape draws run through the vertex stage: six per quad,
130    /// six per band segment. A tiling GPU stores every vertex's varyings
131    /// before it shades a pixel, so this bounds a frame as fill does.
132    pub shape_vertices: u64,
133    pub shape_passes: u32,
134    /// Shape draws using a general pipeline while their specialization compiles.
135    pub shape_pipeline_fallback_draws: u32,
136    /// Shape draws using a completed specialized pipeline.
137    pub shape_specialized_draws: u32,
138    /// Shape draws laying opaque interiors down in a pass's depth pre-pass.
139    pub shape_interior_draws: u32,
140    /// Render passes that took a depth buffer for their opaque interiors.
141    pub depth_passes: u32,
142    /// Pipelines this process has built, counted since it started rather than
143    /// over the frame. A count that grows while a person is interacting names
144    /// a backend shader compile they waited through.
145    pub pipelines_created: u64,
146    /// Runtime shader draws using a shader's general pipeline while the
147    /// specialization they asked for compiles in the background.
148    pub shader_pipeline_fallback_draws: u32,
149    /// Runtime shader draws using a completed specialized pipeline.
150    pub shader_specialized_draws: u32,
151    pub image_passes: u32,
152    pub text_passes: u32,
153    /// Shape, image, glyph and composite draws recorded this frame. The
154    /// `*_passes` counters above count *batches* or passes, so a single image
155    /// batch reports `image_passes=1` however many images it draws; this
156    /// counts what the driver actually sees. Blur and effect quads are not
157    /// included: they are one draw each and already counted by `blur_passes`
158    /// and `effect_applies`.
159    pub draw_calls: u32,
160    pub text_image_cache_hits: u32,
161    pub text_image_cache_misses: u32,
162    pub text_image_cache_hit_pixels: u64,
163    pub text_image_cache_miss_pixels: u64,
164    pub text_image_raster_bytes: u64,
165    pub text_glyph_atlas_hits: u32,
166    pub text_glyph_atlas_misses: u32,
167    pub text_glyph_atlas_miss_pixels: u64,
168    pub offscreen_pool_size: u32,
169    pub offscreen_pool_bytes: u64,
170    pub text_pool_size: u32,
171    pub layer_cache_size: u32,
172    pub layer_cache_bytes: u64,
173    pub image_cache_size: u32,
174    pub text_cache_size: u32,
175    pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
176    pub top_isolated_layer_count: usize,
177}
178
179impl FrameStatsSnapshot {
180    fn shape_fill_by_class_text(&self) -> String {
181        let mut text = String::new();
182        for (label, pixels) in ShapeFill::LABELS
183            .iter()
184            .zip(self.shape_fill_pixels_by_class)
185        {
186            if pixels > 0 {
187                use std::fmt::Write;
188                let _ = write!(text, " {label}={:.2}MP", pixels as f64 / 1_000_000.0);
189            }
190        }
191        text
192    }
193
194    pub(crate) fn with_command_stats_added(mut self, stats: FrameCommandStats) -> Self {
195        self.submits = self.submits.saturating_add(stats.submit_count);
196        self.encoder_count = self.encoder_count.saturating_add(stats.encoder_count);
197        self.submit_count = self.submit_count.saturating_add(stats.submit_count);
198        self.pass_count = self.pass_count.saturating_add(stats.pass_count);
199        self.pass_pixels = self.pass_pixels.saturating_add(stats.pass_pixels);
200        self.copy_count = self.copy_count.saturating_add(stats.copy_count);
201        self.copy_pixels = self.copy_pixels.saturating_add(stats.copy_pixels);
202        self.transient_texture_bytes = self
203            .transient_texture_bytes
204            .saturating_add(stats.transient_texture_bytes);
205        self.retained_texture_bytes = self
206            .retained_texture_bytes
207            .max(stats.retained_texture_bytes);
208        self.upload_bytes = self.upload_bytes.saturating_add(stats.upload_bytes);
209        self.upload_writes = self.upload_writes.saturating_add(stats.upload_writes);
210        self.offscreen_acquires = self
211            .offscreen_acquires
212            .saturating_add(stats.transient_acquires);
213        self.offscreen_news = self.offscreen_news.saturating_add(stats.transient_news);
214        self
215    }
216
217    /// The largest isolated surface of the frame as `WxH@(x,y)` in device
218    /// pixels and logical origin, or `-`.
219    fn top_isolated_display(&self) -> String {
220        match &self.top_isolated_layers[0] {
221            Some(top) => format!(
222                "{}x{}@({:.0},{:.0})",
223                top.width, top.height, top.logical_rect.x, top.logical_rect.y
224            ),
225            None => "-".to_string(),
226        }
227    }
228
229    pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
230        self.top_isolated_layers
231            .into_iter()
232            .flatten()
233            .take(self.top_isolated_layer_count)
234    }
235
236    fn layer_cache_hit_rate(self) -> f64 {
237        let total = self.layer_cache_hits + self.layer_cache_misses;
238        if total > 0 {
239            (self.layer_cache_hits as f64 / total as f64) * 100.0
240        } else {
241            0.0
242        }
243    }
244
245    fn miss_pixels_by_kind_display(&self) -> String {
246        by_kind_display(&self.layer_cache_miss_pixels_by_kind, |pixels| {
247            format!("{:.2}MP", pixels as f64 / 1_000_000.0)
248        })
249    }
250
251    fn hits_by_kind_display(&self) -> String {
252        by_kind_display(&self.layer_cache_hits_by_kind, |hits| hits.to_string())
253    }
254
255    fn print(self, frame_count: u64) {
256        let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
257        let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
258        let retained_mb = self.retained_texture_bytes as f64 / (1024.0 * 1024.0);
259        let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
260        let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
261        let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
262        let shadow_cache_hit_mpx = self.shadow_shape_cache_hit_pixels as f64 / 1_000_000.0;
263        let shadow_cache_miss_mpx = self.shadow_shape_cache_miss_pixels as f64 / 1_000_000.0;
264        let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
265        let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
266        stats_line!(
267            "[GPU f#{}] encoders={} submits={} passes={} pass_px={:.2}MP copies={} copy_px={:.2}MP | offscreen: acq={} new={} {:.1}MB pool={}({:.1}MB) retained={:.1}MB | \
268             uploads={:.2}MB writes={} | \
269             isolated_layers={} area={:.2}MP top={} | \
270             layer_cache: hit={} miss={} {:.1}% hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) hit_by_kind={} miss_px_by_kind={} | \
271             shadow_cache: shape_hit={} shape_miss={} hit_px={:.2}MP miss_px={:.2}MP text_blur_fallback={} | \
272             stages={} admit={} blur={} substrate={} composite={} effect={} shader_px={:.2}MP glass_raster_px={:.2}MP blur_px={:.2}MP | shape={} shape_fill_px={:.2}MP{} shape_verts={} interiors={} depth_passes={} image={} text={} draws={} | \
273             text_img_cache: hit={} miss={} hit_px={:.2}MP miss_px={:.2}MP raster={:.2}MB | \
274             text_glyph_atlas: hit={} miss={} miss_px={:.2}MP | \
275             caches: text_pool={} img={} txt={}",
276            frame_count,
277            self.encoder_count,
278            self.submit_count,
279            self.pass_count,
280            self.pass_pixels as f64 / 1_000_000.0,
281            self.copy_count,
282            self.copy_pixels as f64 / 1_000_000.0,
283            self.offscreen_acquires,
284            self.offscreen_news,
285            mb,
286            self.offscreen_pool_size,
287            pool_mb,
288            retained_mb,
289            upload_mb,
290            self.upload_writes,
291            self.isolated_layer_renders,
292            isolated_layer_mpx,
293            self.top_isolated_display(),
294            self.layer_cache_hits,
295            self.layer_cache_misses,
296            self.layer_cache_hit_rate(),
297            layer_cache_hit_mpx,
298            layer_cache_miss_mpx,
299            self.layer_cache_size,
300            layer_cache_mb,
301            self.hits_by_kind_display(),
302            self.miss_pixels_by_kind_display(),
303            self.shadow_shape_cache_hits,
304            self.shadow_shape_cache_misses,
305            shadow_cache_hit_mpx,
306            shadow_cache_miss_mpx,
307            self.shadow_text_blur_fallbacks,
308            self.stages,
309            self.backdrop_admissions,
310            self.blur_passes,
311            self.substrates,
312            self.composite_passes,
313            self.effect_applies,
314            self.shader_pixels as f64 / 1_000_000.0,
315            self.glass_rasterized_pixels as f64 / 1_000_000.0,
316            self.blur_pixels as f64 / 1_000_000.0,
317            self.shape_passes,
318            self.shape_fill_pixels as f64 / 1_000_000.0,
319            self.shape_fill_by_class_text(),
320            self.shape_vertices,
321            self.shape_interior_draws,
322            self.depth_passes,
323            self.image_passes,
324            self.text_passes,
325            self.draw_calls,
326            self.text_image_cache_hits,
327            self.text_image_cache_misses,
328            self.text_image_cache_hit_pixels as f64 / 1_000_000.0,
329            self.text_image_cache_miss_pixels as f64 / 1_000_000.0,
330            self.text_image_raster_bytes as f64 / (1024.0 * 1024.0),
331            self.text_glyph_atlas_hits,
332            self.text_glyph_atlas_misses,
333            self.text_glyph_atlas_miss_pixels as f64 / 1_000_000.0,
334            self.text_pool_size,
335            self.image_cache_size,
336            self.text_cache_size,
337        );
338        for (index, layer) in self.top_isolated_layers().enumerate() {
339            stats_line!(
340                "  [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{}",
341                layer.node_id,
342                layer.logical_rect.x,
343                layer.logical_rect.y,
344                layer.logical_rect.width,
345                layer.logical_rect.height,
346                layer.width,
347                layer.height,
348            );
349        }
350    }
351}
352
353#[derive(Default)]
354pub(crate) struct FrameStats {
355    pub submits: Cell<u32>,
356    pub command_encoder_count: Cell<u32>,
357    pub command_submit_count: Cell<u32>,
358    pub command_pass_count: Cell<u32>,
359    pub command_pass_pixels: Cell<u64>,
360    pub command_copy_count: Cell<u32>,
361    pub command_copy_pixels: Cell<u64>,
362    pub command_transient_texture_bytes: Cell<u64>,
363    pub command_retained_texture_bytes: Cell<u64>,
364    pub command_upload_bytes: Cell<u64>,
365    pub offscreen_acquires: Cell<u32>,
366    pub offscreen_news: Cell<u32>,
367    pub offscreen_total_bytes: Cell<u64>,
368    pub upload_writes: Cell<u32>,
369    pub isolated_layer_renders: Cell<u32>,
370    pub isolated_layer_pixels: Cell<u64>,
371    pub layer_cache_hits: Cell<u32>,
372    pub capture_fixup_passes: Cell<u32>,
373    pub layer_cache_misses: Cell<u32>,
374    pub layer_cache_hit_pixels: Cell<u64>,
375    pub layer_cache_miss_pixels: Cell<u64>,
376    pub layer_cache_hits_by_kind: [Cell<u32>; LAYER_RASTER_CACHE_KIND_COUNT],
377    pub layer_cache_misses_by_kind: [Cell<u32>; LAYER_RASTER_CACHE_KIND_COUNT],
378    pub layer_cache_miss_pixels_by_kind: [Cell<u64>; LAYER_RASTER_CACHE_KIND_COUNT],
379    pub shadow_shape_cache_hits: Cell<u32>,
380    pub shadow_shape_cache_misses: Cell<u32>,
381    pub shadow_shape_cache_hit_pixels: Cell<u64>,
382    pub shadow_shape_cache_miss_pixels: Cell<u64>,
383    pub shadow_fully_occluded_composites: Cell<u32>,
384    pub shadow_text_blur_fallbacks: Cell<u32>,
385    pub blur_passes: Cell<u32>,
386    pub stages: Cell<u32>,
387    pub backdrop_admissions: Cell<u32>,
388    pub prefix_admissions: Cell<u32>,
389    pub substrates: Cell<u32>,
390    pub composite_passes: Cell<u32>,
391    pub effect_applies: Cell<u32>,
392    pub shader_pixels: Cell<u64>,
393    pub glass_rasterized_pixels: Cell<u64>,
394    pub blur_pixels: Cell<u64>,
395    pub shape_fill_pixels: Cell<u64>,
396    pub shape_fill_pixels_by_class: Cell<[u64; ShapeFill::CLASSES]>,
397    pub shape_vertices: Cell<u64>,
398    pub shape_passes: Cell<u32>,
399    pub shape_pipeline_fallback_draws: Cell<u32>,
400    pub shape_specialized_draws: Cell<u32>,
401    pub shape_interior_draws: Cell<u32>,
402    pub depth_passes: Cell<u32>,
403    pub shader_pipeline_fallback_draws: Cell<u32>,
404    pub shader_specialized_draws: Cell<u32>,
405    pub image_passes: Cell<u32>,
406    pub text_passes: Cell<u32>,
407    pub draw_calls: Cell<u32>,
408    pub text_image_cache_hits: Cell<u32>,
409    pub text_image_cache_misses: Cell<u32>,
410    pub text_image_cache_hit_pixels: Cell<u64>,
411    pub text_image_cache_miss_pixels: Cell<u64>,
412    pub text_image_raster_bytes: Cell<u64>,
413    pub text_glyph_atlas_hits: Cell<u32>,
414    pub text_glyph_atlas_misses: Cell<u32>,
415    pub text_glyph_atlas_miss_pixels: Cell<u64>,
416    pub offscreen_pool_size: Cell<u32>,
417    pub offscreen_pool_bytes: Cell<u64>,
418    pub text_pool_size: Cell<u32>,
419    pub layer_cache_size: Cell<u32>,
420    pub layer_cache_bytes: Cell<u64>,
421    pub image_cache_size: Cell<u32>,
422    pub text_cache_size: Cell<u32>,
423    top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
424    top_isolated_layer_count: Cell<usize>,
425    shadow_shape_cache_miss_log_count: Cell<u32>,
426}
427
428impl FrameStats {
429    pub fn record_capture_fixup_pass(&self) {
430        self.capture_fixup_passes
431            .set(self.capture_fixup_passes.get().saturating_add(1));
432    }
433
434    pub fn record_stages(&self, count: u32) {
435        self.stages.set(self.stages.get().saturating_add(count));
436    }
437
438    pub fn record_backdrop_admission(&self) {
439        self.backdrop_admissions
440            .set(self.backdrop_admissions.get().saturating_add(1));
441    }
442
443    pub fn record_prefix_admission(&self) {
444        self.prefix_admissions
445            .set(self.prefix_admissions.get().saturating_add(1));
446    }
447
448    pub fn record_command_stats(&self, stats: FrameCommandStats) {
449        self.submits
450            .set(self.submits.get().saturating_add(stats.submit_count));
451        self.command_encoder_count.set(
452            self.command_encoder_count
453                .get()
454                .saturating_add(stats.encoder_count),
455        );
456        self.command_submit_count.set(
457            self.command_submit_count
458                .get()
459                .saturating_add(stats.submit_count),
460        );
461        self.command_pass_count.set(
462            self.command_pass_count
463                .get()
464                .saturating_add(stats.pass_count),
465        );
466        self.command_pass_pixels.set(
467            self.command_pass_pixels
468                .get()
469                .saturating_add(stats.pass_pixels),
470        );
471        self.command_copy_count.set(
472            self.command_copy_count
473                .get()
474                .saturating_add(stats.copy_count),
475        );
476        self.command_copy_pixels.set(
477            self.command_copy_pixels
478                .get()
479                .saturating_add(stats.copy_pixels),
480        );
481        self.command_transient_texture_bytes.set(
482            self.command_transient_texture_bytes
483                .get()
484                .saturating_add(stats.transient_texture_bytes),
485        );
486        self.command_retained_texture_bytes.set(
487            self.command_retained_texture_bytes
488                .get()
489                .max(stats.retained_texture_bytes),
490        );
491        self.command_upload_bytes.set(
492            self.command_upload_bytes
493                .get()
494                .saturating_add(stats.upload_bytes),
495        );
496        self.upload_writes
497            .set(self.upload_writes.get().saturating_add(stats.upload_writes));
498        self.offscreen_acquires.set(
499            self.offscreen_acquires
500                .get()
501                .saturating_add(stats.transient_acquires),
502        );
503        self.offscreen_news.set(
504            self.offscreen_news
505                .get()
506                .saturating_add(stats.transient_news),
507        );
508    }
509
510    pub fn record_offscreen_acquire(
511        &self,
512        width: u32,
513        height: u32,
514        format: wgpu::TextureFormat,
515        is_new: bool,
516    ) {
517        self.offscreen_acquires
518            .set(self.offscreen_acquires.get() + 1);
519        if is_new {
520            self.offscreen_news.set(self.offscreen_news.get() + 1);
521        }
522        self.offscreen_total_bytes.set(
523            self.offscreen_total_bytes.get()
524                + (width as u64)
525                    * (height as u64)
526                    * crate::frame_graph::texture_format_bytes_per_pixel(format),
527        );
528    }
529
530    pub fn record_isolated_layer_render(
531        &self,
532        width: u32,
533        height: u32,
534        node_id: Option<NodeId>,
535        logical_rect: Rect,
536    ) {
537        self.isolated_layer_renders
538            .set(self.isolated_layer_renders.get().saturating_add(1));
539        self.isolated_layer_pixels.set(
540            self.isolated_layer_pixels
541                .get()
542                .saturating_add((width as u64) * (height as u64)),
543        );
544        self.record_top_isolated_layer(IsolatedLayerStat {
545            node_id,
546            logical_rect,
547            width,
548            height,
549        });
550    }
551
552    pub fn record_layer_cache_hit(&self, key: &LayerRasterCacheKey, width: u32, height: u32) {
553        let by_kind = &self.layer_cache_hits_by_kind[key.kind_slot()];
554        by_kind.set(by_kind.get().saturating_add(1));
555        self.layer_cache_hits
556            .set(self.layer_cache_hits.get().saturating_add(1));
557        self.layer_cache_hit_pixels.set(
558            self.layer_cache_hit_pixels
559                .get()
560                .saturating_add((width as u64) * (height as u64)),
561        );
562    }
563
564    pub fn record_layer_cache_miss(&self, key: &LayerRasterCacheKey, width: u32, height: u32) {
565        let slot = key.kind_slot();
566        let by_kind = &self.layer_cache_misses_by_kind[slot];
567        by_kind.set(by_kind.get().saturating_add(1));
568        let pixels_by_kind = &self.layer_cache_miss_pixels_by_kind[slot];
569        pixels_by_kind.set(
570            pixels_by_kind
571                .get()
572                .saturating_add((width as u64) * (height as u64)),
573        );
574        self.layer_cache_misses
575            .set(self.layer_cache_misses.get().saturating_add(1));
576        self.layer_cache_miss_pixels.set(
577            self.layer_cache_miss_pixels
578                .get()
579                .saturating_add((width as u64) * (height as u64)),
580        );
581    }
582
583    pub fn record_shadow_shape_cache_hit(&self, composited_pixels: u64) {
584        self.shadow_shape_cache_hits
585            .set(self.shadow_shape_cache_hits.get().saturating_add(1));
586        self.shadow_shape_cache_hit_pixels.set(
587            self.shadow_shape_cache_hit_pixels
588                .get()
589                .saturating_add(composited_pixels),
590        );
591    }
592
593    pub fn record_shadow_fully_occluded(&self) {
594        self.shadow_fully_occluded_composites.set(
595            self.shadow_fully_occluded_composites
596                .get()
597                .saturating_add(1),
598        );
599    }
600
601    pub fn record_shadow_shape_cache_miss(&self, width: u32, height: u32) {
602        self.shadow_shape_cache_misses
603            .set(self.shadow_shape_cache_misses.get().saturating_add(1));
604        self.shadow_shape_cache_miss_pixels.set(
605            self.shadow_shape_cache_miss_pixels
606                .get()
607                .saturating_add((width as u64) * (height as u64)),
608        );
609    }
610
611    #[expect(clippy::too_many_arguments)]
612    pub fn maybe_print_shadow_shape_cache_miss(
613        &self,
614        width: u32,
615        height: u32,
616        content_hash: u64,
617        blur_radius: f32,
618        viewport_offset: [f32; 2],
619        shape_count: usize,
620        clip: Option<Rect>,
621    ) {
622        if !shadow_cache_diagnostics_enabled() {
623            return;
624        }
625
626        let count = self.shadow_shape_cache_miss_log_count.get();
627        if count >= 16 {
628            return;
629        }
630        self.shadow_shape_cache_miss_log_count.set(count + 1);
631
632        let clip_text = clip.map_or_else(
633            || "none".to_string(),
634            |clip| {
635                format!(
636                    "({:.1},{:.1},{:.1},{:.1})",
637                    clip.x, clip.y, clip.width, clip.height
638                )
639            },
640        );
641        stats_line!(
642            "[shadow-cache-miss #{count}] size={}x{} content_hash={content_hash} blur={:.2} viewport_offset=({:.1},{:.1}) shapes={} clip={}",
643            width,
644            height,
645            blur_radius,
646            viewport_offset[0],
647            viewport_offset[1],
648            shape_count,
649            clip_text,
650        );
651    }
652
653    pub fn record_shadow_text_blur_fallback(&self) {
654        self.shadow_text_blur_fallbacks
655            .set(self.shadow_text_blur_fallbacks.get().saturating_add(1));
656    }
657
658    pub fn add_shape_fill(&self, fill: ShapeFill) {
659        self.shape_fill_pixels.set(
660            self.shape_fill_pixels
661                .get()
662                .saturating_add(fill.total().round() as u64),
663        );
664        let mut by_class = self.shape_fill_pixels_by_class.get();
665        for (total, pixels) in by_class.iter_mut().zip(fill.pixels) {
666            *total = total.saturating_add(pixels.round() as u64);
667        }
668        self.shape_fill_pixels_by_class.set(by_class);
669        self.shape_vertices
670            .set(self.shape_vertices.get().saturating_add(fill.vertices));
671    }
672
673    pub fn bump_shapes(&self) {
674        self.shape_passes.set(self.shape_passes.get() + 1);
675    }
676
677    pub fn bump_images(&self) {
678        self.image_passes.set(self.image_passes.get() + 1);
679    }
680
681    pub fn add_draw_calls(&self, count: u32) {
682        self.draw_calls
683            .set(self.draw_calls.get().saturating_add(count));
684    }
685
686    pub fn bump_text(&self) {
687        self.text_passes.set(self.text_passes.get() + 1);
688    }
689
690    pub fn record_text_image_cache_hit(&self, width: u32, height: u32) {
691        self.text_image_cache_hits
692            .set(self.text_image_cache_hits.get().saturating_add(1));
693        self.text_image_cache_hit_pixels.set(
694            self.text_image_cache_hit_pixels
695                .get()
696                .saturating_add((width as u64) * (height as u64)),
697        );
698    }
699
700    pub fn record_text_image_cache_miss(&self, width: u32, height: u32) {
701        let pixels = (width as u64) * (height as u64);
702        self.text_image_cache_misses
703            .set(self.text_image_cache_misses.get().saturating_add(1));
704        self.text_image_cache_miss_pixels.set(
705            self.text_image_cache_miss_pixels
706                .get()
707                .saturating_add(pixels),
708        );
709        self.text_image_raster_bytes.set(
710            self.text_image_raster_bytes
711                .get()
712                .saturating_add(pixels * 4),
713        );
714    }
715
716    pub fn record_text_glyph_atlas_hits(&self, count: u32) {
717        self.text_glyph_atlas_hits
718            .set(self.text_glyph_atlas_hits.get().saturating_add(count));
719    }
720
721    pub fn record_text_glyph_atlas_miss(&self, width: u32, height: u32) {
722        self.text_glyph_atlas_misses
723            .set(self.text_glyph_atlas_misses.get().saturating_add(1));
724        self.text_glyph_atlas_miss_pixels.set(
725            self.text_glyph_atlas_miss_pixels
726                .get()
727                .saturating_add((width as u64) * (height as u64)),
728        );
729    }
730
731    pub fn snapshot(&self) -> FrameStatsSnapshot {
732        let retained_texture_bytes = self
733            .offscreen_pool_bytes
734            .get()
735            .saturating_add(self.layer_cache_bytes.get());
736        FrameStatsSnapshot {
737            submits: self.submits.get(),
738            encoder_count: self.command_encoder_count.get(),
739            submit_count: self.command_submit_count.get(),
740            pass_count: self.command_pass_count.get(),
741            pass_pixels: self.command_pass_pixels.get(),
742            copy_count: self.command_copy_count.get(),
743            copy_pixels: self.command_copy_pixels.get(),
744            offscreen_acquires: self.offscreen_acquires.get(),
745            offscreen_news: self.offscreen_news.get(),
746            offscreen_total_bytes: self.offscreen_total_bytes.get(),
747            transient_texture_bytes: self
748                .offscreen_total_bytes
749                .get()
750                .saturating_add(self.command_transient_texture_bytes.get()),
751            retained_texture_bytes: retained_texture_bytes
752                .saturating_add(self.command_retained_texture_bytes.get()),
753            upload_bytes: self.command_upload_bytes.get(),
754            upload_writes: self.upload_writes.get(),
755            isolated_layer_renders: self.isolated_layer_renders.get(),
756            isolated_layer_pixels: self.isolated_layer_pixels.get(),
757            layer_cache_hits: self.layer_cache_hits.get(),
758            capture_fixup_passes: self.capture_fixup_passes.get(),
759            layer_cache_misses: self.layer_cache_misses.get(),
760            layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
761            layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
762            layer_cache_hits_by_kind: self.layer_cache_hits_by_kind.each_ref().map(Cell::get),
763            layer_cache_misses_by_kind: self.layer_cache_misses_by_kind.each_ref().map(Cell::get),
764            layer_cache_miss_pixels_by_kind: self
765                .layer_cache_miss_pixels_by_kind
766                .each_ref()
767                .map(Cell::get),
768            shadow_shape_cache_hits: self.shadow_shape_cache_hits.get(),
769            shadow_shape_cache_misses: self.shadow_shape_cache_misses.get(),
770            shadow_shape_cache_hit_pixels: self.shadow_shape_cache_hit_pixels.get(),
771            shadow_shape_cache_miss_pixels: self.shadow_shape_cache_miss_pixels.get(),
772            shadow_fully_occluded_composites: self.shadow_fully_occluded_composites.get(),
773            shadow_text_blur_fallbacks: self.shadow_text_blur_fallbacks.get(),
774            blur_passes: self.blur_passes.get(),
775            stages: self.stages.get(),
776            backdrop_admissions: self.backdrop_admissions.get(),
777            prefix_admissions: self.prefix_admissions.get(),
778            substrates: self.substrates.get(),
779            composite_passes: self.composite_passes.get(),
780            effect_applies: self.effect_applies.get(),
781            shader_pixels: self.shader_pixels.get(),
782            glass_rasterized_pixels: self.glass_rasterized_pixels.get(),
783            blur_pixels: self.blur_pixels.get(),
784            shape_fill_pixels: self.shape_fill_pixels.get(),
785            shape_fill_pixels_by_class: self.shape_fill_pixels_by_class.get(),
786            shape_vertices: self.shape_vertices.get(),
787            shape_passes: self.shape_passes.get(),
788            shape_pipeline_fallback_draws: self.shape_pipeline_fallback_draws.get(),
789            shape_specialized_draws: self.shape_specialized_draws.get(),
790            shape_interior_draws: self.shape_interior_draws.get(),
791            depth_passes: self.depth_passes.get(),
792            pipelines_created: crate::render::pipelines_created(),
793            shader_pipeline_fallback_draws: self.shader_pipeline_fallback_draws.get(),
794            shader_specialized_draws: self.shader_specialized_draws.get(),
795            image_passes: self.image_passes.get(),
796            text_passes: self.text_passes.get(),
797            draw_calls: self.draw_calls.get(),
798            text_image_cache_hits: self.text_image_cache_hits.get(),
799            text_image_cache_misses: self.text_image_cache_misses.get(),
800            text_image_cache_hit_pixels: self.text_image_cache_hit_pixels.get(),
801            text_image_cache_miss_pixels: self.text_image_cache_miss_pixels.get(),
802            text_image_raster_bytes: self.text_image_raster_bytes.get(),
803            text_glyph_atlas_hits: self.text_glyph_atlas_hits.get(),
804            text_glyph_atlas_misses: self.text_glyph_atlas_misses.get(),
805            text_glyph_atlas_miss_pixels: self.text_glyph_atlas_miss_pixels.get(),
806            offscreen_pool_size: self.offscreen_pool_size.get(),
807            offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
808            text_pool_size: self.text_pool_size.get(),
809            layer_cache_size: self.layer_cache_size.get(),
810            layer_cache_bytes: self.layer_cache_bytes.get(),
811            image_cache_size: self.image_cache_size.get(),
812            text_cache_size: self.text_cache_size.get(),
813            top_isolated_layers: *self.top_isolated_layers.borrow(),
814            top_isolated_layer_count: self.top_isolated_layer_count.get(),
815        }
816    }
817
818    pub fn reset(&self) {
819        self.submits.set(0);
820        self.command_encoder_count.set(0);
821        self.command_submit_count.set(0);
822        self.command_pass_count.set(0);
823        self.command_pass_pixels.set(0);
824        self.command_copy_count.set(0);
825        self.command_copy_pixels.set(0);
826        self.command_transient_texture_bytes.set(0);
827        self.command_retained_texture_bytes.set(0);
828        self.command_upload_bytes.set(0);
829        self.upload_writes.set(0);
830        self.offscreen_acquires.set(0);
831        self.offscreen_news.set(0);
832        self.offscreen_total_bytes.set(0);
833        self.isolated_layer_renders.set(0);
834        self.isolated_layer_pixels.set(0);
835        self.layer_cache_hits.set(0);
836        self.capture_fixup_passes.set(0);
837        self.layer_cache_misses.set(0);
838        self.layer_cache_hit_pixels.set(0);
839        self.layer_cache_miss_pixels.set(0);
840        for slot in 0..LAYER_RASTER_CACHE_KIND_COUNT {
841            self.layer_cache_hits_by_kind[slot].set(0);
842            self.layer_cache_misses_by_kind[slot].set(0);
843            self.layer_cache_miss_pixels_by_kind[slot].set(0);
844        }
845        self.shadow_shape_cache_hits.set(0);
846        self.shadow_shape_cache_misses.set(0);
847        self.shadow_shape_cache_hit_pixels.set(0);
848        self.shadow_shape_cache_miss_pixels.set(0);
849        self.shadow_fully_occluded_composites.set(0);
850        self.shadow_text_blur_fallbacks.set(0);
851        self.blur_passes.set(0);
852        self.stages.set(0);
853        self.backdrop_admissions.set(0);
854        self.prefix_admissions.set(0);
855        self.substrates.set(0);
856        self.composite_passes.set(0);
857        self.effect_applies.set(0);
858        self.shader_pixels.set(0);
859        self.glass_rasterized_pixels.set(0);
860        self.blur_pixels.set(0);
861        self.shape_fill_pixels.set(0);
862        self.shape_fill_pixels_by_class.set([0; ShapeFill::CLASSES]);
863        self.shape_vertices.set(0);
864        self.shape_passes.set(0);
865        self.shape_pipeline_fallback_draws.set(0);
866        self.shape_specialized_draws.set(0);
867        self.shape_interior_draws.set(0);
868        self.depth_passes.set(0);
869        self.shader_pipeline_fallback_draws.set(0);
870        self.shader_specialized_draws.set(0);
871        self.image_passes.set(0);
872        self.text_passes.set(0);
873        self.draw_calls.set(0);
874        self.text_image_cache_hits.set(0);
875        self.text_image_cache_misses.set(0);
876        self.text_image_cache_hit_pixels.set(0);
877        self.text_image_cache_miss_pixels.set(0);
878        self.text_image_raster_bytes.set(0);
879        self.text_glyph_atlas_hits.set(0);
880        self.text_glyph_atlas_misses.set(0);
881        self.text_glyph_atlas_miss_pixels.set(0);
882        *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
883        self.top_isolated_layer_count.set(0);
884        self.shadow_shape_cache_miss_log_count.set(0);
885    }
886
887    pub fn maybe_print_snapshot(
888        &self,
889        snapshot: FrameStatsSnapshot,
890        frame_count: &mut u64,
891        enabled: bool,
892    ) {
893        if !enabled {
894            return;
895        }
896        *frame_count += 1;
897        if (*frame_count).is_multiple_of(60) {
898            snapshot.print(*frame_count);
899        }
900    }
901
902    fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
903        let mut top_layers = self.top_isolated_layers.borrow_mut();
904        let len = self.top_isolated_layer_count.get();
905        let insert_at = top_layers[..len]
906            .iter()
907            .enumerate()
908            .find_map(|(index, existing)| {
909                existing
910                    .filter(|existing| layer.pixel_area() > existing.pixel_area())
911                    .map(|_| index)
912            })
913            .unwrap_or(len);
914
915        if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
916            return;
917        }
918
919        let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
920            len + 1
921        } else {
922            TOP_ISOLATED_LAYER_LIMIT
923        };
924
925        let mut index = new_len.saturating_sub(1);
926        while index > insert_at {
927            top_layers[index] = top_layers[index - 1];
928            index -= 1;
929        }
930        top_layers[insert_at] = Some(layer);
931        self.top_isolated_layer_count.set(new_len);
932    }
933}
934
935static GPU_STATS: DebugToggle = DebugToggle::new("CRANPOSE_GPU_STATS");
936
937pub(crate) fn gpu_stats_enabled() -> bool {
938    GPU_STATS.flag()
939}
940
941pub(crate) fn print_gpu_memory_report(device: &wgpu::Device, frame_count: u64) {
942    let Some(report) = device.generate_allocator_report() else {
943        return;
944    };
945
946    const MB: f64 = 1024.0 * 1024.0;
947    let mut blocks = String::new();
948    for block in &report.blocks {
949        if !blocks.is_empty() {
950            blocks.push('+');
951        }
952        blocks.push_str(&format!("{:.1}", block.size as f64 / MB));
953    }
954
955    stats_line!(
956        "[GPU-MEM f#{}] reserved={:.1}MB allocated={:.1}MB blocks={}[{}MB] allocations={} | largest={:.6?}",
957        frame_count,
958        report.total_reserved_bytes as f64 / MB,
959        report.total_allocated_bytes as f64 / MB,
960        report.blocks.len(),
961        blocks,
962        report.allocations.len(),
963        report,
964    );
965}
966
967static SHADOW_CACHE_DIAG: DebugToggle = DebugToggle::new("CRANPOSE_GPU_SHADOW_CACHE_DIAG");
968
969fn shadow_cache_diagnostics_enabled() -> bool {
970    SHADOW_CACHE_DIAG.flag()
971}
972
973fn by_kind_display<T: Copy + Default + PartialEq>(
974    by_kind: &[T; LAYER_RASTER_CACHE_KIND_COUNT],
975    format: impl Fn(T) -> String,
976) -> String {
977    LAYER_RASTER_CACHE_KIND_LABELS
978        .iter()
979        .zip(by_kind)
980        .filter(|(_, value)| **value != T::default())
981        .map(|(label, value)| format!("{label}={}", format(*value)))
982        .collect::<Vec<_>>()
983        .join(",")
984}
985
986#[cfg(test)]
987#[path = "tests/gpu_stats_tests.rs"]
988mod tests;