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    /// Pipelines this process has built, counted since it started rather than
128    /// over the frame. A count that grows while a person is interacting names
129    /// a backend shader compile they waited through.
130    pub pipelines_created: u64,
131    /// Runtime shader draws using a shader's general pipeline while the
132    /// specialization they asked for compiles in the background.
133    pub shader_pipeline_fallback_draws: u32,
134    /// Runtime shader draws using a completed specialized pipeline.
135    pub shader_specialized_draws: u32,
136    pub image_passes: u32,
137    pub text_passes: u32,
138    /// Shape, image, glyph and composite draws recorded this frame. The
139    /// `*_passes` counters above count *batches* or passes, so a single image
140    /// batch reports `image_passes=1` however many images it draws; this
141    /// counts what the driver actually sees. Blur and effect quads are not
142    /// included: they are one draw each and already counted by `blur_passes`
143    /// and `effect_applies`.
144    pub draw_calls: u32,
145    pub text_image_cache_hits: u32,
146    pub text_image_cache_misses: u32,
147    pub text_image_cache_hit_pixels: u64,
148    pub text_image_cache_miss_pixels: u64,
149    pub text_image_raster_bytes: u64,
150    pub text_glyph_atlas_hits: u32,
151    pub text_glyph_atlas_misses: u32,
152    pub text_glyph_atlas_miss_pixels: u64,
153    pub offscreen_pool_size: u32,
154    pub offscreen_pool_bytes: u64,
155    pub text_pool_size: u32,
156    pub layer_cache_size: u32,
157    pub layer_cache_bytes: u64,
158    pub image_cache_size: u32,
159    pub text_cache_size: u32,
160    pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
161    pub top_isolated_layer_count: usize,
162}
163
164impl FrameStatsSnapshot {
165    fn shape_fill_by_class_text(&self) -> String {
166        let mut text = String::new();
167        for (label, pixels) in ShapeFill::LABELS
168            .iter()
169            .zip(self.shape_fill_pixels_by_class)
170        {
171            if pixels > 0 {
172                use std::fmt::Write;
173                let _ = write!(text, " {label}={:.2}MP", pixels as f64 / 1_000_000.0);
174            }
175        }
176        text
177    }
178
179    pub(crate) fn with_command_stats_added(mut self, stats: FrameCommandStats) -> Self {
180        self.submits = self.submits.saturating_add(stats.submit_count);
181        self.encoder_count = self.encoder_count.saturating_add(stats.encoder_count);
182        self.submit_count = self.submit_count.saturating_add(stats.submit_count);
183        self.pass_count = self.pass_count.saturating_add(stats.pass_count);
184        self.pass_pixels = self.pass_pixels.saturating_add(stats.pass_pixels);
185        self.copy_count = self.copy_count.saturating_add(stats.copy_count);
186        self.copy_pixels = self.copy_pixels.saturating_add(stats.copy_pixels);
187        self.transient_texture_bytes = self
188            .transient_texture_bytes
189            .saturating_add(stats.transient_texture_bytes);
190        self.retained_texture_bytes = self
191            .retained_texture_bytes
192            .max(stats.retained_texture_bytes);
193        self.upload_bytes = self.upload_bytes.saturating_add(stats.upload_bytes);
194        self.upload_writes = self.upload_writes.saturating_add(stats.upload_writes);
195        self.offscreen_acquires = self
196            .offscreen_acquires
197            .saturating_add(stats.transient_acquires);
198        self.offscreen_news = self.offscreen_news.saturating_add(stats.transient_news);
199        self
200    }
201
202    /// The largest isolated surface of the frame as `WxH@(x,y)` in device
203    /// pixels and logical origin, or `-`.
204    fn top_isolated_display(&self) -> String {
205        match &self.top_isolated_layers[0] {
206            Some(top) => format!(
207                "{}x{}@({:.0},{:.0})",
208                top.width, top.height, top.logical_rect.x, top.logical_rect.y
209            ),
210            None => "-".to_string(),
211        }
212    }
213
214    pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
215        self.top_isolated_layers
216            .into_iter()
217            .flatten()
218            .take(self.top_isolated_layer_count)
219    }
220
221    fn layer_cache_hit_rate(self) -> f64 {
222        let total = self.layer_cache_hits + self.layer_cache_misses;
223        if total > 0 {
224            (self.layer_cache_hits as f64 / total as f64) * 100.0
225        } else {
226            0.0
227        }
228    }
229
230    fn miss_pixels_by_kind_display(&self) -> String {
231        by_kind_display(&self.layer_cache_miss_pixels_by_kind, |pixels| {
232            format!("{:.2}MP", pixels as f64 / 1_000_000.0)
233        })
234    }
235
236    fn hits_by_kind_display(&self) -> String {
237        by_kind_display(&self.layer_cache_hits_by_kind, |hits| hits.to_string())
238    }
239
240    fn print(self, frame_count: u64) {
241        let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
242        let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
243        let retained_mb = self.retained_texture_bytes as f64 / (1024.0 * 1024.0);
244        let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
245        let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
246        let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
247        let shadow_cache_hit_mpx = self.shadow_shape_cache_hit_pixels as f64 / 1_000_000.0;
248        let shadow_cache_miss_mpx = self.shadow_shape_cache_miss_pixels as f64 / 1_000_000.0;
249        let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
250        let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
251        eprintln!(
252            "[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 | \
253             uploads={:.2}MB writes={} | \
254             isolated_layers={} area={:.2}MP top={} | \
255             layer_cache: hit={} miss={} {:.1}% hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) hit_by_kind={} miss_px_by_kind={} | \
256             shadow_cache: shape_hit={} shape_miss={} hit_px={:.2}MP miss_px={:.2}MP text_blur_fallback={} | \
257             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={} image={} text={} draws={} | \
258             text_img_cache: hit={} miss={} hit_px={:.2}MP miss_px={:.2}MP raster={:.2}MB | \
259             text_glyph_atlas: hit={} miss={} miss_px={:.2}MP | \
260             caches: text_pool={} img={} txt={}",
261            frame_count,
262            self.encoder_count,
263            self.submit_count,
264            self.pass_count,
265            self.pass_pixels as f64 / 1_000_000.0,
266            self.copy_count,
267            self.copy_pixels as f64 / 1_000_000.0,
268            self.offscreen_acquires,
269            self.offscreen_news,
270            mb,
271            self.offscreen_pool_size,
272            pool_mb,
273            retained_mb,
274            upload_mb,
275            self.upload_writes,
276            self.isolated_layer_renders,
277            isolated_layer_mpx,
278            self.top_isolated_display(),
279            self.layer_cache_hits,
280            self.layer_cache_misses,
281            self.layer_cache_hit_rate(),
282            layer_cache_hit_mpx,
283            layer_cache_miss_mpx,
284            self.layer_cache_size,
285            layer_cache_mb,
286            self.hits_by_kind_display(),
287            self.miss_pixels_by_kind_display(),
288            self.shadow_shape_cache_hits,
289            self.shadow_shape_cache_misses,
290            shadow_cache_hit_mpx,
291            shadow_cache_miss_mpx,
292            self.shadow_text_blur_fallbacks,
293            self.stages,
294            self.backdrop_admissions,
295            self.blur_passes,
296            self.substrates,
297            self.composite_passes,
298            self.effect_applies,
299            self.shader_pixels as f64 / 1_000_000.0,
300            self.glass_rasterized_pixels as f64 / 1_000_000.0,
301            self.blur_pixels as f64 / 1_000_000.0,
302            self.shape_passes,
303            self.shape_fill_pixels as f64 / 1_000_000.0,
304            self.shape_fill_by_class_text(),
305            self.shape_vertices,
306            self.image_passes,
307            self.text_passes,
308            self.draw_calls,
309            self.text_image_cache_hits,
310            self.text_image_cache_misses,
311            self.text_image_cache_hit_pixels as f64 / 1_000_000.0,
312            self.text_image_cache_miss_pixels as f64 / 1_000_000.0,
313            self.text_image_raster_bytes as f64 / (1024.0 * 1024.0),
314            self.text_glyph_atlas_hits,
315            self.text_glyph_atlas_misses,
316            self.text_glyph_atlas_miss_pixels as f64 / 1_000_000.0,
317            self.text_pool_size,
318            self.image_cache_size,
319            self.text_cache_size,
320        );
321        for (index, layer) in self.top_isolated_layers().enumerate() {
322            eprintln!(
323                "  [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{}",
324                layer.node_id,
325                layer.logical_rect.x,
326                layer.logical_rect.y,
327                layer.logical_rect.width,
328                layer.logical_rect.height,
329                layer.width,
330                layer.height,
331            );
332        }
333    }
334}
335
336#[derive(Default)]
337pub(crate) struct FrameStats {
338    pub submits: Cell<u32>,
339    pub command_encoder_count: Cell<u32>,
340    pub command_submit_count: Cell<u32>,
341    pub command_pass_count: Cell<u32>,
342    pub command_pass_pixels: Cell<u64>,
343    pub command_copy_count: Cell<u32>,
344    pub command_copy_pixels: Cell<u64>,
345    pub command_transient_texture_bytes: Cell<u64>,
346    pub command_retained_texture_bytes: Cell<u64>,
347    pub command_upload_bytes: Cell<u64>,
348    pub offscreen_acquires: Cell<u32>,
349    pub offscreen_news: Cell<u32>,
350    pub offscreen_total_bytes: Cell<u64>,
351    pub upload_writes: Cell<u32>,
352    pub isolated_layer_renders: Cell<u32>,
353    pub isolated_layer_pixels: Cell<u64>,
354    pub layer_cache_hits: Cell<u32>,
355    pub capture_fixup_passes: Cell<u32>,
356    pub layer_cache_misses: Cell<u32>,
357    pub layer_cache_hit_pixels: Cell<u64>,
358    pub layer_cache_miss_pixels: Cell<u64>,
359    pub layer_cache_hits_by_kind: [Cell<u32>; LAYER_RASTER_CACHE_KIND_COUNT],
360    pub layer_cache_misses_by_kind: [Cell<u32>; LAYER_RASTER_CACHE_KIND_COUNT],
361    pub layer_cache_miss_pixels_by_kind: [Cell<u64>; LAYER_RASTER_CACHE_KIND_COUNT],
362    pub shadow_shape_cache_hits: Cell<u32>,
363    pub shadow_shape_cache_misses: Cell<u32>,
364    pub shadow_shape_cache_hit_pixels: Cell<u64>,
365    pub shadow_shape_cache_miss_pixels: Cell<u64>,
366    pub shadow_fully_occluded_composites: Cell<u32>,
367    pub shadow_text_blur_fallbacks: Cell<u32>,
368    pub blur_passes: Cell<u32>,
369    pub stages: Cell<u32>,
370    pub backdrop_admissions: Cell<u32>,
371    pub prefix_admissions: Cell<u32>,
372    pub substrates: Cell<u32>,
373    pub composite_passes: Cell<u32>,
374    pub effect_applies: Cell<u32>,
375    pub shader_pixels: Cell<u64>,
376    pub glass_rasterized_pixels: Cell<u64>,
377    pub blur_pixels: Cell<u64>,
378    pub shape_fill_pixels: Cell<u64>,
379    pub shape_fill_pixels_by_class: Cell<[u64; ShapeFill::CLASSES]>,
380    pub shape_vertices: Cell<u64>,
381    pub shape_passes: Cell<u32>,
382    pub shape_pipeline_fallback_draws: Cell<u32>,
383    pub shape_specialized_draws: Cell<u32>,
384    pub shader_pipeline_fallback_draws: Cell<u32>,
385    pub shader_specialized_draws: Cell<u32>,
386    pub image_passes: Cell<u32>,
387    pub text_passes: Cell<u32>,
388    pub draw_calls: Cell<u32>,
389    pub text_image_cache_hits: Cell<u32>,
390    pub text_image_cache_misses: Cell<u32>,
391    pub text_image_cache_hit_pixels: Cell<u64>,
392    pub text_image_cache_miss_pixels: Cell<u64>,
393    pub text_image_raster_bytes: Cell<u64>,
394    pub text_glyph_atlas_hits: Cell<u32>,
395    pub text_glyph_atlas_misses: Cell<u32>,
396    pub text_glyph_atlas_miss_pixels: Cell<u64>,
397    pub offscreen_pool_size: Cell<u32>,
398    pub offscreen_pool_bytes: Cell<u64>,
399    pub text_pool_size: Cell<u32>,
400    pub layer_cache_size: Cell<u32>,
401    pub layer_cache_bytes: Cell<u64>,
402    pub image_cache_size: Cell<u32>,
403    pub text_cache_size: Cell<u32>,
404    top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
405    top_isolated_layer_count: Cell<usize>,
406    shadow_shape_cache_miss_log_count: Cell<u32>,
407}
408
409impl FrameStats {
410    pub fn record_capture_fixup_pass(&self) {
411        self.capture_fixup_passes
412            .set(self.capture_fixup_passes.get().saturating_add(1));
413    }
414
415    pub fn record_stages(&self, count: u32) {
416        self.stages.set(self.stages.get().saturating_add(count));
417    }
418
419    pub fn record_backdrop_admission(&self) {
420        self.backdrop_admissions
421            .set(self.backdrop_admissions.get().saturating_add(1));
422    }
423
424    pub fn record_prefix_admission(&self) {
425        self.prefix_admissions
426            .set(self.prefix_admissions.get().saturating_add(1));
427    }
428
429    pub fn record_command_stats(&self, stats: FrameCommandStats) {
430        self.submits
431            .set(self.submits.get().saturating_add(stats.submit_count));
432        self.command_encoder_count.set(
433            self.command_encoder_count
434                .get()
435                .saturating_add(stats.encoder_count),
436        );
437        self.command_submit_count.set(
438            self.command_submit_count
439                .get()
440                .saturating_add(stats.submit_count),
441        );
442        self.command_pass_count.set(
443            self.command_pass_count
444                .get()
445                .saturating_add(stats.pass_count),
446        );
447        self.command_pass_pixels.set(
448            self.command_pass_pixels
449                .get()
450                .saturating_add(stats.pass_pixels),
451        );
452        self.command_copy_count.set(
453            self.command_copy_count
454                .get()
455                .saturating_add(stats.copy_count),
456        );
457        self.command_copy_pixels.set(
458            self.command_copy_pixels
459                .get()
460                .saturating_add(stats.copy_pixels),
461        );
462        self.command_transient_texture_bytes.set(
463            self.command_transient_texture_bytes
464                .get()
465                .saturating_add(stats.transient_texture_bytes),
466        );
467        self.command_retained_texture_bytes.set(
468            self.command_retained_texture_bytes
469                .get()
470                .max(stats.retained_texture_bytes),
471        );
472        self.command_upload_bytes.set(
473            self.command_upload_bytes
474                .get()
475                .saturating_add(stats.upload_bytes),
476        );
477        self.upload_writes
478            .set(self.upload_writes.get().saturating_add(stats.upload_writes));
479        self.offscreen_acquires.set(
480            self.offscreen_acquires
481                .get()
482                .saturating_add(stats.transient_acquires),
483        );
484        self.offscreen_news.set(
485            self.offscreen_news
486                .get()
487                .saturating_add(stats.transient_news),
488        );
489    }
490
491    pub fn record_offscreen_acquire(
492        &self,
493        width: u32,
494        height: u32,
495        format: wgpu::TextureFormat,
496        is_new: bool,
497    ) {
498        self.offscreen_acquires
499            .set(self.offscreen_acquires.get() + 1);
500        if is_new {
501            self.offscreen_news.set(self.offscreen_news.get() + 1);
502        }
503        self.offscreen_total_bytes.set(
504            self.offscreen_total_bytes.get()
505                + (width as u64)
506                    * (height as u64)
507                    * crate::frame_graph::texture_format_bytes_per_pixel(format),
508        );
509    }
510
511    pub fn record_isolated_layer_render(
512        &self,
513        width: u32,
514        height: u32,
515        node_id: Option<NodeId>,
516        logical_rect: Rect,
517    ) {
518        self.isolated_layer_renders
519            .set(self.isolated_layer_renders.get().saturating_add(1));
520        self.isolated_layer_pixels.set(
521            self.isolated_layer_pixels
522                .get()
523                .saturating_add((width as u64) * (height as u64)),
524        );
525        self.record_top_isolated_layer(IsolatedLayerStat {
526            node_id,
527            logical_rect,
528            width,
529            height,
530        });
531    }
532
533    pub fn record_layer_cache_hit(&self, key: &LayerRasterCacheKey, width: u32, height: u32) {
534        let by_kind = &self.layer_cache_hits_by_kind[key.kind_slot()];
535        by_kind.set(by_kind.get().saturating_add(1));
536        self.layer_cache_hits
537            .set(self.layer_cache_hits.get().saturating_add(1));
538        self.layer_cache_hit_pixels.set(
539            self.layer_cache_hit_pixels
540                .get()
541                .saturating_add((width as u64) * (height as u64)),
542        );
543    }
544
545    pub fn record_layer_cache_miss(&self, key: &LayerRasterCacheKey, width: u32, height: u32) {
546        let slot = key.kind_slot();
547        let by_kind = &self.layer_cache_misses_by_kind[slot];
548        by_kind.set(by_kind.get().saturating_add(1));
549        let pixels_by_kind = &self.layer_cache_miss_pixels_by_kind[slot];
550        pixels_by_kind.set(
551            pixels_by_kind
552                .get()
553                .saturating_add((width as u64) * (height as u64)),
554        );
555        self.layer_cache_misses
556            .set(self.layer_cache_misses.get().saturating_add(1));
557        self.layer_cache_miss_pixels.set(
558            self.layer_cache_miss_pixels
559                .get()
560                .saturating_add((width as u64) * (height as u64)),
561        );
562    }
563
564    pub fn record_shadow_shape_cache_hit(&self, composited_pixels: u64) {
565        self.shadow_shape_cache_hits
566            .set(self.shadow_shape_cache_hits.get().saturating_add(1));
567        self.shadow_shape_cache_hit_pixels.set(
568            self.shadow_shape_cache_hit_pixels
569                .get()
570                .saturating_add(composited_pixels),
571        );
572    }
573
574    pub fn record_shadow_fully_occluded(&self) {
575        self.shadow_fully_occluded_composites.set(
576            self.shadow_fully_occluded_composites
577                .get()
578                .saturating_add(1),
579        );
580    }
581
582    pub fn record_shadow_shape_cache_miss(&self, width: u32, height: u32) {
583        self.shadow_shape_cache_misses
584            .set(self.shadow_shape_cache_misses.get().saturating_add(1));
585        self.shadow_shape_cache_miss_pixels.set(
586            self.shadow_shape_cache_miss_pixels
587                .get()
588                .saturating_add((width as u64) * (height as u64)),
589        );
590    }
591
592    #[allow(clippy::too_many_arguments)]
593    pub fn maybe_print_shadow_shape_cache_miss(
594        &self,
595        width: u32,
596        height: u32,
597        content_hash: u64,
598        blur_radius: f32,
599        viewport_offset: [f32; 2],
600        shape_count: usize,
601        clip: Option<Rect>,
602    ) {
603        if !shadow_cache_diagnostics_enabled() {
604            return;
605        }
606
607        let count = self.shadow_shape_cache_miss_log_count.get();
608        if count >= 16 {
609            return;
610        }
611        self.shadow_shape_cache_miss_log_count.set(count + 1);
612
613        let clip_text = clip.map_or_else(
614            || "none".to_string(),
615            |clip| {
616                format!(
617                    "({:.1},{:.1},{:.1},{:.1})",
618                    clip.x, clip.y, clip.width, clip.height
619                )
620            },
621        );
622        eprintln!(
623            "[shadow-cache-miss #{count}] size={}x{} content_hash={content_hash} blur={:.2} viewport_offset=({:.1},{:.1}) shapes={} clip={}",
624            width,
625            height,
626            blur_radius,
627            viewport_offset[0],
628            viewport_offset[1],
629            shape_count,
630            clip_text,
631        );
632    }
633
634    pub fn record_shadow_text_blur_fallback(&self) {
635        self.shadow_text_blur_fallbacks
636            .set(self.shadow_text_blur_fallbacks.get().saturating_add(1));
637    }
638
639    pub fn add_shape_fill(&self, fill: ShapeFill) {
640        self.shape_fill_pixels.set(
641            self.shape_fill_pixels
642                .get()
643                .saturating_add(fill.total().round() as u64),
644        );
645        let mut by_class = self.shape_fill_pixels_by_class.get();
646        for (total, pixels) in by_class.iter_mut().zip(fill.pixels) {
647            *total = total.saturating_add(pixels.round() as u64);
648        }
649        self.shape_fill_pixels_by_class.set(by_class);
650        self.shape_vertices
651            .set(self.shape_vertices.get().saturating_add(fill.vertices));
652    }
653
654    pub fn bump_shapes(&self) {
655        self.shape_passes.set(self.shape_passes.get() + 1);
656    }
657
658    pub fn bump_images(&self) {
659        self.image_passes.set(self.image_passes.get() + 1);
660    }
661
662    pub fn add_draw_calls(&self, count: u32) {
663        self.draw_calls
664            .set(self.draw_calls.get().saturating_add(count));
665    }
666
667    pub fn bump_text(&self) {
668        self.text_passes.set(self.text_passes.get() + 1);
669    }
670
671    pub fn record_text_image_cache_hit(&self, width: u32, height: u32) {
672        self.text_image_cache_hits
673            .set(self.text_image_cache_hits.get().saturating_add(1));
674        self.text_image_cache_hit_pixels.set(
675            self.text_image_cache_hit_pixels
676                .get()
677                .saturating_add((width as u64) * (height as u64)),
678        );
679    }
680
681    pub fn record_text_image_cache_miss(&self, width: u32, height: u32) {
682        let pixels = (width as u64) * (height as u64);
683        self.text_image_cache_misses
684            .set(self.text_image_cache_misses.get().saturating_add(1));
685        self.text_image_cache_miss_pixels.set(
686            self.text_image_cache_miss_pixels
687                .get()
688                .saturating_add(pixels),
689        );
690        self.text_image_raster_bytes.set(
691            self.text_image_raster_bytes
692                .get()
693                .saturating_add(pixels * 4),
694        );
695    }
696
697    pub fn record_text_glyph_atlas_hits(&self, count: u32) {
698        self.text_glyph_atlas_hits
699            .set(self.text_glyph_atlas_hits.get().saturating_add(count));
700    }
701
702    pub fn record_text_glyph_atlas_miss(&self, width: u32, height: u32) {
703        self.text_glyph_atlas_misses
704            .set(self.text_glyph_atlas_misses.get().saturating_add(1));
705        self.text_glyph_atlas_miss_pixels.set(
706            self.text_glyph_atlas_miss_pixels
707                .get()
708                .saturating_add((width as u64) * (height as u64)),
709        );
710    }
711
712    pub fn snapshot(&self) -> FrameStatsSnapshot {
713        let retained_texture_bytes = self
714            .offscreen_pool_bytes
715            .get()
716            .saturating_add(self.layer_cache_bytes.get());
717        FrameStatsSnapshot {
718            submits: self.submits.get(),
719            encoder_count: self.command_encoder_count.get(),
720            submit_count: self.command_submit_count.get(),
721            pass_count: self.command_pass_count.get(),
722            pass_pixels: self.command_pass_pixels.get(),
723            copy_count: self.command_copy_count.get(),
724            copy_pixels: self.command_copy_pixels.get(),
725            offscreen_acquires: self.offscreen_acquires.get(),
726            offscreen_news: self.offscreen_news.get(),
727            offscreen_total_bytes: self.offscreen_total_bytes.get(),
728            transient_texture_bytes: self
729                .offscreen_total_bytes
730                .get()
731                .saturating_add(self.command_transient_texture_bytes.get()),
732            retained_texture_bytes: retained_texture_bytes
733                .saturating_add(self.command_retained_texture_bytes.get()),
734            upload_bytes: self.command_upload_bytes.get(),
735            upload_writes: self.upload_writes.get(),
736            isolated_layer_renders: self.isolated_layer_renders.get(),
737            isolated_layer_pixels: self.isolated_layer_pixels.get(),
738            layer_cache_hits: self.layer_cache_hits.get(),
739            capture_fixup_passes: self.capture_fixup_passes.get(),
740            layer_cache_misses: self.layer_cache_misses.get(),
741            layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
742            layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
743            layer_cache_hits_by_kind: self.layer_cache_hits_by_kind.each_ref().map(Cell::get),
744            layer_cache_misses_by_kind: self.layer_cache_misses_by_kind.each_ref().map(Cell::get),
745            layer_cache_miss_pixels_by_kind: self
746                .layer_cache_miss_pixels_by_kind
747                .each_ref()
748                .map(Cell::get),
749            shadow_shape_cache_hits: self.shadow_shape_cache_hits.get(),
750            shadow_shape_cache_misses: self.shadow_shape_cache_misses.get(),
751            shadow_shape_cache_hit_pixels: self.shadow_shape_cache_hit_pixels.get(),
752            shadow_shape_cache_miss_pixels: self.shadow_shape_cache_miss_pixels.get(),
753            shadow_fully_occluded_composites: self.shadow_fully_occluded_composites.get(),
754            shadow_text_blur_fallbacks: self.shadow_text_blur_fallbacks.get(),
755            blur_passes: self.blur_passes.get(),
756            stages: self.stages.get(),
757            backdrop_admissions: self.backdrop_admissions.get(),
758            prefix_admissions: self.prefix_admissions.get(),
759            substrates: self.substrates.get(),
760            composite_passes: self.composite_passes.get(),
761            effect_applies: self.effect_applies.get(),
762            shader_pixels: self.shader_pixels.get(),
763            glass_rasterized_pixels: self.glass_rasterized_pixels.get(),
764            blur_pixels: self.blur_pixels.get(),
765            shape_fill_pixels: self.shape_fill_pixels.get(),
766            shape_fill_pixels_by_class: self.shape_fill_pixels_by_class.get(),
767            shape_vertices: self.shape_vertices.get(),
768            shape_passes: self.shape_passes.get(),
769            shape_pipeline_fallback_draws: self.shape_pipeline_fallback_draws.get(),
770            shape_specialized_draws: self.shape_specialized_draws.get(),
771            pipelines_created: crate::render::pipelines_created(),
772            shader_pipeline_fallback_draws: self.shader_pipeline_fallback_draws.get(),
773            shader_specialized_draws: self.shader_specialized_draws.get(),
774            image_passes: self.image_passes.get(),
775            text_passes: self.text_passes.get(),
776            draw_calls: self.draw_calls.get(),
777            text_image_cache_hits: self.text_image_cache_hits.get(),
778            text_image_cache_misses: self.text_image_cache_misses.get(),
779            text_image_cache_hit_pixels: self.text_image_cache_hit_pixels.get(),
780            text_image_cache_miss_pixels: self.text_image_cache_miss_pixels.get(),
781            text_image_raster_bytes: self.text_image_raster_bytes.get(),
782            text_glyph_atlas_hits: self.text_glyph_atlas_hits.get(),
783            text_glyph_atlas_misses: self.text_glyph_atlas_misses.get(),
784            text_glyph_atlas_miss_pixels: self.text_glyph_atlas_miss_pixels.get(),
785            offscreen_pool_size: self.offscreen_pool_size.get(),
786            offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
787            text_pool_size: self.text_pool_size.get(),
788            layer_cache_size: self.layer_cache_size.get(),
789            layer_cache_bytes: self.layer_cache_bytes.get(),
790            image_cache_size: self.image_cache_size.get(),
791            text_cache_size: self.text_cache_size.get(),
792            top_isolated_layers: *self.top_isolated_layers.borrow(),
793            top_isolated_layer_count: self.top_isolated_layer_count.get(),
794        }
795    }
796
797    pub fn reset(&self) {
798        self.submits.set(0);
799        self.command_encoder_count.set(0);
800        self.command_submit_count.set(0);
801        self.command_pass_count.set(0);
802        self.command_pass_pixels.set(0);
803        self.command_copy_count.set(0);
804        self.command_copy_pixels.set(0);
805        self.command_transient_texture_bytes.set(0);
806        self.command_retained_texture_bytes.set(0);
807        self.command_upload_bytes.set(0);
808        self.upload_writes.set(0);
809        self.offscreen_acquires.set(0);
810        self.offscreen_news.set(0);
811        self.offscreen_total_bytes.set(0);
812        self.isolated_layer_renders.set(0);
813        self.isolated_layer_pixels.set(0);
814        self.layer_cache_hits.set(0);
815        self.capture_fixup_passes.set(0);
816        self.layer_cache_misses.set(0);
817        self.layer_cache_hit_pixels.set(0);
818        self.layer_cache_miss_pixels.set(0);
819        for slot in 0..LAYER_RASTER_CACHE_KIND_COUNT {
820            self.layer_cache_hits_by_kind[slot].set(0);
821            self.layer_cache_misses_by_kind[slot].set(0);
822            self.layer_cache_miss_pixels_by_kind[slot].set(0);
823        }
824        self.shadow_shape_cache_hits.set(0);
825        self.shadow_shape_cache_misses.set(0);
826        self.shadow_shape_cache_hit_pixels.set(0);
827        self.shadow_shape_cache_miss_pixels.set(0);
828        self.shadow_fully_occluded_composites.set(0);
829        self.shadow_text_blur_fallbacks.set(0);
830        self.blur_passes.set(0);
831        self.stages.set(0);
832        self.backdrop_admissions.set(0);
833        self.prefix_admissions.set(0);
834        self.substrates.set(0);
835        self.composite_passes.set(0);
836        self.effect_applies.set(0);
837        self.shader_pixels.set(0);
838        self.glass_rasterized_pixels.set(0);
839        self.blur_pixels.set(0);
840        self.shape_fill_pixels.set(0);
841        self.shape_fill_pixels_by_class.set([0; ShapeFill::CLASSES]);
842        self.shape_vertices.set(0);
843        self.shape_passes.set(0);
844        self.shape_pipeline_fallback_draws.set(0);
845        self.shape_specialized_draws.set(0);
846        self.shader_pipeline_fallback_draws.set(0);
847        self.shader_specialized_draws.set(0);
848        self.image_passes.set(0);
849        self.text_passes.set(0);
850        self.draw_calls.set(0);
851        self.text_image_cache_hits.set(0);
852        self.text_image_cache_misses.set(0);
853        self.text_image_cache_hit_pixels.set(0);
854        self.text_image_cache_miss_pixels.set(0);
855        self.text_image_raster_bytes.set(0);
856        self.text_glyph_atlas_hits.set(0);
857        self.text_glyph_atlas_misses.set(0);
858        self.text_glyph_atlas_miss_pixels.set(0);
859        *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
860        self.top_isolated_layer_count.set(0);
861        self.shadow_shape_cache_miss_log_count.set(0);
862    }
863
864    pub fn maybe_print_snapshot(
865        &self,
866        snapshot: FrameStatsSnapshot,
867        frame_count: &mut u64,
868        enabled: bool,
869    ) {
870        if !enabled {
871            return;
872        }
873        *frame_count += 1;
874        if (*frame_count).is_multiple_of(60) {
875            snapshot.print(*frame_count);
876        }
877    }
878
879    fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
880        let mut top_layers = self.top_isolated_layers.borrow_mut();
881        let len = self.top_isolated_layer_count.get();
882        let insert_at = top_layers[..len]
883            .iter()
884            .enumerate()
885            .find_map(|(index, existing)| {
886                existing
887                    .filter(|existing| layer.pixel_area() > existing.pixel_area())
888                    .map(|_| index)
889            })
890            .unwrap_or(len);
891
892        if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
893            return;
894        }
895
896        let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
897            len + 1
898        } else {
899            TOP_ISOLATED_LAYER_LIMIT
900        };
901
902        let mut index = new_len.saturating_sub(1);
903        while index > insert_at {
904            top_layers[index] = top_layers[index - 1];
905            index -= 1;
906        }
907        top_layers[insert_at] = Some(layer);
908        self.top_isolated_layer_count.set(new_len);
909    }
910}
911
912static GPU_STATS: DebugToggle = DebugToggle::new("CRANPOSE_GPU_STATS");
913
914pub(crate) fn gpu_stats_enabled() -> bool {
915    GPU_STATS.flag()
916}
917
918pub(crate) fn print_gpu_memory_report(device: &wgpu::Device, frame_count: u64) {
919    let Some(report) = device.generate_allocator_report() else {
920        return;
921    };
922
923    const MB: f64 = 1024.0 * 1024.0;
924    let mut blocks = String::new();
925    for block in &report.blocks {
926        if !blocks.is_empty() {
927            blocks.push('+');
928        }
929        blocks.push_str(&format!("{:.1}", block.size as f64 / MB));
930    }
931
932    eprintln!(
933        "[GPU-MEM f#{}] reserved={:.1}MB allocated={:.1}MB blocks={}[{}MB] allocations={} | largest={:.6?}",
934        frame_count,
935        report.total_reserved_bytes as f64 / MB,
936        report.total_allocated_bytes as f64 / MB,
937        report.blocks.len(),
938        blocks,
939        report.allocations.len(),
940        report,
941    );
942}
943
944static SHADOW_CACHE_DIAG: DebugToggle = DebugToggle::new("CRANPOSE_GPU_SHADOW_CACHE_DIAG");
945
946fn shadow_cache_diagnostics_enabled() -> bool {
947    SHADOW_CACHE_DIAG.flag()
948}
949
950fn by_kind_display<T: Copy + Default + PartialEq>(
951    by_kind: &[T; LAYER_RASTER_CACHE_KIND_COUNT],
952    format: impl Fn(T) -> String,
953) -> String {
954    LAYER_RASTER_CACHE_KIND_LABELS
955        .iter()
956        .zip(by_kind)
957        .filter(|(_, value)| **value != T::default())
958        .map(|(label, value)| format!("{label}={}", format(*value)))
959        .collect::<Vec<_>>()
960        .join(",")
961}
962
963#[cfg(test)]
964mod tests {
965    use super::*;
966
967    fn test_layer_cache_key() -> LayerRasterCacheKey {
968        LayerRasterCacheKey::source_content(
969            None,
970            0,
971            Rect {
972                x: 0.0,
973                y: 0.0,
974                width: 5.0,
975                height: 6.0,
976            },
977            (5, 6),
978            cranpose_render_common::raster_cache::ScaleBucket::from_scale(1.0),
979            cranpose_ui_graphics::Point::default(),
980        )
981    }
982
983    #[test]
984    fn layer_cache_counters_accumulate_and_reset() {
985        let stats = FrameStats::default();
986        stats.record_command_stats(FrameCommandStats {
987            encoder_count: 1,
988            submit_count: 1,
989            pass_count: 2,
990            transient_texture_bytes: 256,
991            retained_texture_bytes: 128,
992            upload_bytes: 64,
993            ..FrameCommandStats::default()
994        });
995        stats.bump_shapes();
996        stats.shape_pipeline_fallback_draws.set(3);
997        stats.shape_specialized_draws.set(5);
998        stats.shader_pipeline_fallback_draws.set(2);
999        stats.shader_specialized_draws.set(4);
1000        stats.blur_passes.set(1);
1001        stats.offscreen_total_bytes.set(1024);
1002        stats.offscreen_pool_bytes.set(2048);
1003        stats.record_layer_cache_hit(&test_layer_cache_key(), 10, 20);
1004        stats.record_layer_cache_hit(&test_layer_cache_key(), 3, 4);
1005        stats.record_layer_cache_miss(&test_layer_cache_key(), 5, 6);
1006        stats.record_shadow_shape_cache_hit(72);
1007        stats.record_shadow_shape_cache_miss(10, 11);
1008        stats.record_shadow_text_blur_fallback();
1009        stats.record_text_image_cache_hit(13, 17);
1010        stats.record_text_image_cache_miss(19, 23);
1011
1012        assert_eq!(stats.layer_cache_hits.get(), 2);
1013        assert_eq!(stats.layer_cache_misses.get(), 1);
1014        assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
1015        assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
1016        assert_eq!(stats.shadow_shape_cache_hits.get(), 1);
1017        assert_eq!(stats.shadow_shape_cache_misses.get(), 1);
1018        assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 72);
1019        assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 110);
1020        assert_eq!(stats.shadow_text_blur_fallbacks.get(), 1);
1021
1022        stats.record_isolated_layer_render(
1023            7,
1024            8,
1025            Some(9),
1026            Rect {
1027                x: 2.0,
1028                y: 3.0,
1029                width: 4.0,
1030                height: 5.0,
1031            },
1032        );
1033        let snapshot = stats.snapshot();
1034
1035        assert_eq!(snapshot.shape_pipeline_fallback_draws, 3);
1036        assert_eq!(snapshot.shape_specialized_draws, 5);
1037        assert_eq!(snapshot.shader_pipeline_fallback_draws, 2);
1038        assert_eq!(snapshot.shader_specialized_draws, 4);
1039        assert_eq!(snapshot.isolated_layer_renders, 1);
1040        assert_eq!(snapshot.isolated_layer_pixels, 56);
1041        assert_eq!(snapshot.upload_bytes, 64);
1042        assert_eq!(snapshot.encoder_count, 1);
1043        assert_eq!(snapshot.submit_count, 1);
1044        assert_eq!(snapshot.pass_count, 2);
1045        assert_eq!(snapshot.transient_texture_bytes, 1280);
1046        assert_eq!(snapshot.retained_texture_bytes, 2176);
1047        assert_eq!(snapshot.layer_cache_hits, 2);
1048        assert_eq!(snapshot.layer_cache_misses, 1);
1049        assert_eq!(snapshot.shadow_shape_cache_hits, 1);
1050        assert_eq!(snapshot.shadow_shape_cache_misses, 1);
1051        assert_eq!(snapshot.shadow_shape_cache_hit_pixels, 72);
1052        assert_eq!(snapshot.shadow_shape_cache_miss_pixels, 110);
1053        assert_eq!(snapshot.shadow_text_blur_fallbacks, 1);
1054        assert_eq!(snapshot.text_image_cache_hits, 1);
1055        assert_eq!(snapshot.text_image_cache_misses, 1);
1056        assert_eq!(snapshot.text_image_cache_hit_pixels, 221);
1057        assert_eq!(snapshot.text_image_cache_miss_pixels, 437);
1058        assert_eq!(snapshot.text_image_raster_bytes, 1748);
1059        let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
1060        assert_eq!(top_layers.len(), 1);
1061        assert_eq!(top_layers[0].node_id, Some(9));
1062        assert_eq!(stats.layer_cache_hits.get(), 2);
1063        assert_eq!(stats.layer_cache_misses.get(), 1);
1064
1065        stats.reset();
1066
1067        assert_eq!(stats.snapshot().shape_pipeline_fallback_draws, 0);
1068        assert_eq!(stats.snapshot().shape_specialized_draws, 0);
1069        assert_eq!(stats.snapshot().shader_pipeline_fallback_draws, 0);
1070        assert_eq!(stats.snapshot().shader_specialized_draws, 0);
1071        assert_eq!(stats.layer_cache_hits.get(), 0);
1072        assert_eq!(stats.layer_cache_misses.get(), 0);
1073        assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
1074        assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
1075        assert_eq!(stats.shadow_shape_cache_hits.get(), 0);
1076        assert_eq!(stats.shadow_shape_cache_misses.get(), 0);
1077        assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 0);
1078        assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 0);
1079        assert_eq!(stats.shadow_text_blur_fallbacks.get(), 0);
1080        assert_eq!(stats.text_image_cache_hits.get(), 0);
1081        assert_eq!(stats.text_image_cache_misses.get(), 0);
1082        assert_eq!(stats.text_image_cache_hit_pixels.get(), 0);
1083        assert_eq!(stats.text_image_cache_miss_pixels.get(), 0);
1084        assert_eq!(stats.text_image_raster_bytes.get(), 0);
1085        assert_eq!(stats.isolated_layer_renders.get(), 0);
1086        assert_eq!(stats.isolated_layer_pixels.get(), 0);
1087        assert_eq!(stats.top_isolated_layer_count.get(), 0);
1088    }
1089
1090    #[test]
1091    fn command_stats_accumulate_and_reset() {
1092        let stats = FrameStats::default();
1093
1094        stats.record_command_stats(FrameCommandStats {
1095            encoder_count: 2,
1096            submit_count: 2,
1097            pass_count: 5,
1098            transient_texture_bytes: 1024,
1099            retained_texture_bytes: 2048,
1100            upload_bytes: 512,
1101            ..FrameCommandStats::default()
1102        });
1103        stats.bump_shapes();
1104
1105        let snapshot = stats.snapshot();
1106        assert_eq!(snapshot.submits, 2);
1107        assert_eq!(snapshot.encoder_count, 2);
1108        assert_eq!(snapshot.submit_count, 2);
1109        assert_eq!(snapshot.pass_count, 5);
1110        assert_eq!(snapshot.transient_texture_bytes, 1024);
1111        assert_eq!(snapshot.retained_texture_bytes, 2048);
1112        assert_eq!(snapshot.upload_bytes, 512);
1113
1114        stats.reset();
1115        let reset = stats.snapshot();
1116        assert_eq!(reset.submits, 0);
1117        assert_eq!(reset.encoder_count, 0);
1118        assert_eq!(reset.submit_count, 0);
1119        assert_eq!(reset.pass_count, 0);
1120        assert_eq!(reset.transient_texture_bytes, 0);
1121        assert_eq!(reset.retained_texture_bytes, 0);
1122        assert_eq!(reset.upload_bytes, 0);
1123    }
1124
1125    #[test]
1126    fn snapshot_adds_explicit_readback_command_stats() {
1127        let stats = FrameStats::default();
1128        stats.record_command_stats(FrameCommandStats {
1129            encoder_count: 1,
1130            submit_count: 1,
1131            pass_count: 2,
1132            transient_texture_bytes: 128,
1133            retained_texture_bytes: 512,
1134            upload_bytes: 64,
1135            ..FrameCommandStats::default()
1136        });
1137        let snapshot = stats
1138            .snapshot()
1139            .with_command_stats_added(FrameCommandStats {
1140                encoder_count: 1,
1141                submit_count: 1,
1142                pass_count: 1,
1143                ..FrameCommandStats::default()
1144            });
1145
1146        assert_eq!(snapshot.submits, 2);
1147        assert_eq!(snapshot.encoder_count, 2);
1148        assert_eq!(snapshot.submit_count, 2);
1149        assert_eq!(snapshot.pass_count, 3);
1150        assert_eq!(snapshot.transient_texture_bytes, 128);
1151        assert_eq!(snapshot.retained_texture_bytes, 512);
1152        assert_eq!(snapshot.upload_bytes, 64);
1153    }
1154
1155    #[test]
1156    fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
1157        let stats = FrameStats::default();
1158        let snapshot = stats.snapshot();
1159        let mut frame_count = 0;
1160
1161        stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
1162        assert_eq!(frame_count, 0);
1163
1164        stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
1165        assert_eq!(frame_count, 1);
1166    }
1167
1168    #[test]
1169    fn top_isolated_layers_keep_largest_runtime_surfaces() {
1170        let stats = FrameStats::default();
1171        for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
1172            stats.record_isolated_layer_render(
1173                16 + index as u32,
1174                8 + index as u32,
1175                Some(index),
1176                Rect {
1177                    x: index as f32,
1178                    y: 0.0,
1179                    width: 10.0,
1180                    height: 10.0,
1181                },
1182            );
1183        }
1184
1185        let snapshot = stats.snapshot();
1186        let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
1187        assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
1188        assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
1189        assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
1190    }
1191}