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