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
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    /// Sum of every render pass's first color target area: the tile traffic a
50    /// tiling GPU pays whatever the passes draw.
51    pub pass_pixels: u64,
52    /// Texture regions copied outside any pass, and their texels.
53    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    /// Buffer writes the frame staged: uploads that reach the GPU as one
62    /// write each, however many draws they carry.
63    pub upload_writes: u32,
64    pub isolated_layer_renders: u32,
65    pub isolated_layer_pixels: u64,
66    pub layer_cache_hits: u32,
67    /// Passes that drew fix-ups over copied capture regions.
68    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    /// Hits per key kind, indexed by `LayerRasterCacheKey::kind_slot`.
73    pub layer_cache_hits_by_kind: [u32; LAYER_RASTER_CACHE_KIND_COUNT],
74    /// Stores per key kind, indexed by `LayerRasterCacheKey::kind_slot`.
75    pub layer_cache_misses_by_kind: [u32; LAYER_RASTER_CACHE_KIND_COUNT],
76    /// Stored pixels per key kind, indexed by `LayerRasterCacheKey::kind_slot`.
77    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    /// Converted shadows whose caster occluded every visible coverage pixel
83    /// this frame: they composite nothing and their draw item is dropped.
84    pub shadow_fully_occluded_composites: u32,
85    pub shadow_text_blur_fallbacks: u32,
86    pub blur_passes: u32,
87    /// Backdrop stages resolved: every stage flushes the page and captures
88    /// once, so the layer pass count is one more than this.
89    pub stages: u32,
90    /// Backdrops whose stage texture the layer cache pinned this frame, so
91    /// later frames replay their composites from it.
92    pub backdrop_admissions: u32,
93    /// Opaque page prefixes copied into the layer cache this frame.
94    pub prefix_admissions: u32,
95    /// Captures averaged into a quarter-size substrate for a shader's wide
96    /// taps.
97    pub substrates: u32,
98    /// Passes that draw at least one resolved composite: a final pass holding
99    /// twelve glass tails counts once, a composite resolved into its own
100    /// texture counts its own pass.
101    pub composite_passes: u32,
102    pub effect_applies: u32,
103    /// Pixels shaded by runtime shader composites: what every glass surface
104    /// costs a tiling GPU per frame, each capture that re-shades one included.
105    pub shader_pixels: u64,
106    pub glass_rasterized_pixels: u64,
107    /// Pixels the blur passes write: every downsample, horizontal and
108    /// vertical pass of a backdrop's blur and of its substrates, within
109    /// the scissor the pass was given.
110    pub blur_pixels: u64,
111    /// Pixels the shape draws rasterize: a band drawn as a mesh counts its
112    /// mesh, everything else its quad. Fill rate is what a scene of rings
113    /// costs a tiling GPU, however few draws it takes.
114    pub shape_fill_pixels: u64,
115    /// `shape_fill_pixels` split by shape kind and brush, in the order of
116    /// `ShapeFill::LABELS`: which records a fill-bound frame pays for.
117    pub shape_fill_pixels_by_class: [u64; ShapeFill::CLASSES],
118    /// Vertices the shape draws run through the vertex stage: six per quad,
119    /// six per band segment. A tiling GPU stores every vertex's varyings
120    /// before it shades a pixel, so this bounds a frame as fill does.
121    pub shape_vertices: u64,
122    pub shape_passes: u32,
123    /// Shape draws using a general pipeline while their specialization compiles.
124    pub shape_pipeline_fallback_draws: u32,
125    /// Shape draws using a completed specialized pipeline.
126    pub shape_specialized_draws: u32,
127    /// Shape draws laying opaque interiors down in a pass's depth pre-pass.
128    pub shape_interior_draws: u32,
129    /// Pipelines this process has built, counted since it started rather than
130    /// over the frame. A count that grows while a person is interacting names
131    /// a backend shader compile they waited through.
132    pub pipelines_created: u64,
133    /// Runtime shader draws using a shader's general pipeline while the
134    /// specialization they asked for compiles in the background.
135    pub shader_pipeline_fallback_draws: u32,
136    /// Runtime shader draws using a completed specialized pipeline.
137    pub shader_specialized_draws: u32,
138    pub image_passes: u32,
139    pub text_passes: u32,
140    /// Shape, image, glyph and composite draws recorded this frame. The
141    /// `*_passes` counters above count *batches* or passes, so a single image
142    /// batch reports `image_passes=1` however many images it draws; this
143    /// counts what the driver actually sees. Blur and effect quads are not
144    /// included: they are one draw each and already counted by `blur_passes`
145    /// and `effect_applies`.
146    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    /// The largest isolated surface of the frame as `WxH@(x,y)` in device
205    /// pixels and logical origin, or `-`.
206    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;