Skip to main content

cranpose_render_wgpu/
gpu_stats.rs

1//! Per-frame GPU render counters.
2//!
3//! Counters are always collected so tests and perf harnesses can assert them.
4//! Setting `CRANPOSE_GPU_STATS=1` prints a summary line every 60 frames to stderr.
5
6use crate::frame_graph::FrameCommandStats;
7use crate::surface_requirements::{SurfaceRequirement, SurfaceRequirementSet};
8use std::cell::{Cell, RefCell};
9
10use cranpose_core::NodeId;
11use cranpose_ui_graphics::Rect;
12
13const TOP_ISOLATED_LAYER_LIMIT: usize = 8;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct LayerSurfaceReasons {
17    pub explicit_offscreen: bool,
18    pub effect: bool,
19    pub backdrop: bool,
20    pub group_opacity: bool,
21    pub blend_mode: bool,
22    pub shape_clip: bool,
23    pub immediate_shadow: bool,
24    pub text_local_surface: bool,
25    pub motion_stable_capture: bool,
26    pub mixed_direct_content: bool,
27    pub non_translation_transform: bool,
28    pub pixel_stable_composite: bool,
29}
30
31impl LayerSurfaceReasons {
32    /// Returns true if any isolating requirement is set.
33    /// Delegates to `SurfaceRequirementSet::has_isolating_requirement` to
34    /// keep the semantics in sync (e.g. `mixed_direct_content` alone does
35    /// not count as isolating).
36    pub fn has_any(self) -> bool {
37        self.explicit_offscreen
38            || self.effect
39            || self.backdrop
40            || self.group_opacity
41            || self.blend_mode
42            || self.shape_clip
43            || self.text_local_surface
44            || self.motion_stable_capture
45            || self.non_translation_transform
46    }
47
48    pub fn labels(self) -> impl Iterator<Item = &'static str> {
49        let mut labels = [None; 12];
50        let mut len = 0usize;
51
52        if self.explicit_offscreen {
53            labels[len] = Some("explicit_offscreen");
54            len += 1;
55        }
56        if self.effect {
57            labels[len] = Some("effect");
58            len += 1;
59        }
60        if self.backdrop {
61            labels[len] = Some("backdrop");
62            len += 1;
63        }
64        if self.group_opacity {
65            labels[len] = Some("group_opacity");
66            len += 1;
67        }
68        if self.blend_mode {
69            labels[len] = Some("blend_mode");
70            len += 1;
71        }
72        if self.shape_clip {
73            labels[len] = Some("shape_clip");
74            len += 1;
75        }
76        if self.immediate_shadow {
77            labels[len] = Some("immediate_shadow");
78            len += 1;
79        }
80        if self.text_local_surface {
81            labels[len] = Some("text_local_surface");
82            len += 1;
83        }
84        if self.motion_stable_capture {
85            labels[len] = Some("motion_stable_capture");
86            len += 1;
87        }
88        if self.mixed_direct_content {
89            labels[len] = Some("mixed_direct_content");
90            len += 1;
91        }
92        if self.non_translation_transform {
93            labels[len] = Some("non_translation_transform");
94            len += 1;
95        }
96        if self.pixel_stable_composite {
97            labels[len] = Some("pixel_stable_composite");
98            len += 1;
99        }
100
101        labels.into_iter().flatten().take(len)
102    }
103
104    pub fn display(self) -> String {
105        let mut joined = String::new();
106        for (index, label) in self.labels().enumerate() {
107            if index > 0 {
108                joined.push('+');
109            }
110            joined.push_str(label);
111        }
112        if joined.is_empty() {
113            joined.push_str("none");
114        }
115        joined
116    }
117
118    pub fn has_renderer_forced_surface(self) -> bool {
119        self.text_local_surface || self.non_translation_transform
120    }
121}
122
123impl From<SurfaceRequirementSet> for LayerSurfaceReasons {
124    fn from(requirements: SurfaceRequirementSet) -> Self {
125        Self {
126            explicit_offscreen: requirements.contains(SurfaceRequirement::ExplicitOffscreen),
127            effect: requirements.contains(SurfaceRequirement::RenderEffect),
128            backdrop: requirements.contains(SurfaceRequirement::Backdrop),
129            group_opacity: requirements.contains(SurfaceRequirement::GroupOpacity),
130            blend_mode: requirements.contains(SurfaceRequirement::BlendMode),
131            shape_clip: requirements.contains(SurfaceRequirement::ShapeClip),
132            immediate_shadow: requirements.contains(SurfaceRequirement::ImmediateShadow),
133            text_local_surface: requirements.contains(SurfaceRequirement::TextMaterialMask),
134            motion_stable_capture: requirements.contains(SurfaceRequirement::MotionStableCapture),
135            mixed_direct_content: requirements.contains(SurfaceRequirement::MixedDirectContent),
136            non_translation_transform: requirements
137                .contains(SurfaceRequirement::NonTranslationTransform),
138            pixel_stable_composite: requirements.contains(SurfaceRequirement::PixelStableComposite),
139        }
140    }
141}
142
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct IsolatedLayerStat {
145    pub node_id: Option<NodeId>,
146    pub logical_rect: Rect,
147    pub width: u32,
148    pub height: u32,
149    pub reasons: LayerSurfaceReasons,
150}
151
152impl IsolatedLayerStat {
153    fn pixel_area(self) -> u64 {
154        (self.width as u64) * (self.height as u64)
155    }
156}
157
158impl Default for IsolatedLayerStat {
159    fn default() -> Self {
160        Self {
161            node_id: None,
162            logical_rect: Rect {
163                x: 0.0,
164                y: 0.0,
165                width: 0.0,
166                height: 0.0,
167            },
168            width: 0,
169            height: 0,
170            reasons: LayerSurfaceReasons::default(),
171        }
172    }
173}
174
175#[derive(Clone, Copy, Debug, Default, PartialEq)]
176pub struct FrameStatsSnapshot {
177    pub submits: u32,
178    pub encoder_count: u32,
179    pub submit_count: u32,
180    pub pass_count: u32,
181    pub offscreen_acquires: u32,
182    pub offscreen_news: u32,
183    pub offscreen_total_bytes: u64,
184    pub transient_texture_bytes: u64,
185    pub retained_texture_bytes: u64,
186    pub upload_bytes: u64,
187    pub isolated_layer_renders: u32,
188    pub isolated_layer_pixels: u64,
189    pub layer_cache_hits: u32,
190    pub layer_cache_misses: u32,
191    pub layer_cache_evictions: u32,
192    pub layer_cache_hit_pixels: u64,
193    pub layer_cache_miss_pixels: u64,
194    pub shadow_shape_cache_hits: u32,
195    pub shadow_shape_cache_misses: u32,
196    pub shadow_shape_cache_hit_pixels: u64,
197    pub shadow_shape_cache_miss_pixels: u64,
198    pub shadow_text_blur_fallbacks: u32,
199    pub blur_passes: u32,
200    pub composite_passes: u32,
201    pub effect_applies: u32,
202    pub shape_passes: u32,
203    pub image_passes: u32,
204    pub text_passes: u32,
205    pub text_image_cache_hits: u32,
206    pub text_image_cache_misses: u32,
207    pub text_image_cache_hit_pixels: u64,
208    pub text_image_cache_miss_pixels: u64,
209    pub text_image_raster_bytes: u64,
210    pub text_glyph_atlas_hits: u32,
211    pub text_glyph_atlas_misses: u32,
212    pub text_glyph_atlas_miss_pixels: u64,
213    pub offscreen_pool_size: u32,
214    pub offscreen_pool_bytes: u64,
215    pub text_pool_size: u32,
216    pub layer_cache_size: u32,
217    pub layer_cache_bytes: u64,
218    pub image_cache_size: u32,
219    pub text_cache_size: u32,
220    pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
221    pub top_isolated_layer_count: usize,
222}
223
224impl FrameStatsSnapshot {
225    pub(crate) fn with_command_stats_added(mut self, stats: FrameCommandStats) -> Self {
226        self.submits = self.submits.saturating_add(stats.submit_count);
227        self.encoder_count = self.encoder_count.saturating_add(stats.encoder_count);
228        self.submit_count = self.submit_count.saturating_add(stats.submit_count);
229        self.pass_count = self.pass_count.saturating_add(stats.pass_count);
230        self.transient_texture_bytes = self
231            .transient_texture_bytes
232            .saturating_add(stats.transient_texture_bytes);
233        self.retained_texture_bytes = self
234            .retained_texture_bytes
235            .max(stats.retained_texture_bytes);
236        self.upload_bytes = self.upload_bytes.saturating_add(stats.upload_bytes);
237        self
238    }
239
240    pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
241        self.top_isolated_layers
242            .into_iter()
243            .flatten()
244            .take(self.top_isolated_layer_count)
245    }
246
247    fn layer_cache_hit_rate(self) -> f64 {
248        let total = self.layer_cache_hits + self.layer_cache_misses;
249        if total > 0 {
250            (self.layer_cache_hits as f64 / total as f64) * 100.0
251        } else {
252            0.0
253        }
254    }
255
256    fn print(self, frame_count: u64) {
257        let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
258        let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
259        let retained_mb = self.retained_texture_bytes as f64 / (1024.0 * 1024.0);
260        let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
261        let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
262        let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
263        let shadow_cache_hit_mpx = self.shadow_shape_cache_hit_pixels as f64 / 1_000_000.0;
264        let shadow_cache_miss_mpx = self.shadow_shape_cache_miss_pixels as f64 / 1_000_000.0;
265        let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
266        let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
267        eprintln!(
268            "[GPU f#{}] encoders={} submits={} passes={} | offscreen: acq={} new={} {:.1}MB pool={}({:.1}MB) retained={:.1}MB | \
269             uploads={:.2}MB | \
270             isolated_layers={} area={:.2}MP | \
271             layer_cache: hit={} miss={} {:.1}% evict={} hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) | \
272             shadow_cache: shape_hit={} shape_miss={} hit_px={:.2}MP miss_px={:.2}MP text_blur_fallback={} | \
273             blur={} composite={} effect={} | shape={} image={} text={} | \
274             text_img_cache: hit={} miss={} hit_px={:.2}MP miss_px={:.2}MP raster={:.2}MB | \
275             text_glyph_atlas: hit={} miss={} miss_px={:.2}MP | \
276             caches: text_pool={} img={} txt={}",
277            frame_count,
278            self.encoder_count,
279            self.submit_count,
280            self.pass_count,
281            self.offscreen_acquires,
282            self.offscreen_news,
283            mb,
284            self.offscreen_pool_size,
285            pool_mb,
286            retained_mb,
287            upload_mb,
288            self.isolated_layer_renders,
289            isolated_layer_mpx,
290            self.layer_cache_hits,
291            self.layer_cache_misses,
292            self.layer_cache_hit_rate(),
293            self.layer_cache_evictions,
294            layer_cache_hit_mpx,
295            layer_cache_miss_mpx,
296            self.layer_cache_size,
297            layer_cache_mb,
298            self.shadow_shape_cache_hits,
299            self.shadow_shape_cache_misses,
300            shadow_cache_hit_mpx,
301            shadow_cache_miss_mpx,
302            self.shadow_text_blur_fallbacks,
303            self.blur_passes,
304            self.composite_passes,
305            self.effect_applies,
306            self.shape_passes,
307            self.image_passes,
308            self.text_passes,
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{} reasons={}",
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                layer.reasons.display(),
332            );
333        }
334    }
335}
336
337/// Per-frame debug counters for GPU work instrumentation.
338/// Uses `Cell` fields so counters can be bumped through shared references.
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_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_bytes: Cell<u64>,
352    pub isolated_layer_renders: Cell<u32>,
353    pub isolated_layer_pixels: Cell<u64>,
354    pub layer_cache_hits: Cell<u32>,
355    pub layer_cache_misses: Cell<u32>,
356    pub layer_cache_evictions: Cell<u32>,
357    pub layer_cache_hit_pixels: Cell<u64>,
358    pub layer_cache_miss_pixels: Cell<u64>,
359    pub shadow_shape_cache_hits: Cell<u32>,
360    pub shadow_shape_cache_misses: Cell<u32>,
361    pub shadow_shape_cache_hit_pixels: Cell<u64>,
362    pub shadow_shape_cache_miss_pixels: Cell<u64>,
363    pub shadow_text_blur_fallbacks: Cell<u32>,
364    pub blur_passes: Cell<u32>,
365    pub composite_passes: Cell<u32>,
366    pub effect_applies: Cell<u32>,
367    pub shape_passes: Cell<u32>,
368    pub image_passes: Cell<u32>,
369    pub text_passes: Cell<u32>,
370    pub text_image_cache_hits: Cell<u32>,
371    pub text_image_cache_misses: Cell<u32>,
372    pub text_image_cache_hit_pixels: Cell<u64>,
373    pub text_image_cache_miss_pixels: Cell<u64>,
374    pub text_image_raster_bytes: Cell<u64>,
375    pub text_glyph_atlas_hits: Cell<u32>,
376    pub text_glyph_atlas_misses: Cell<u32>,
377    pub text_glyph_atlas_miss_pixels: Cell<u64>,
378    // Pool/cache sizes snapshotted at end of frame
379    pub offscreen_pool_size: Cell<u32>,
380    pub offscreen_pool_bytes: Cell<u64>,
381    pub text_pool_size: Cell<u32>,
382    pub layer_cache_size: Cell<u32>,
383    pub layer_cache_bytes: Cell<u64>,
384    pub image_cache_size: Cell<u32>,
385    pub text_cache_size: Cell<u32>,
386    top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
387    top_isolated_layer_count: Cell<usize>,
388    shadow_shape_cache_miss_log_count: Cell<u32>,
389}
390
391impl FrameStats {
392    pub fn record_command_stats(&self, stats: FrameCommandStats) {
393        self.submits
394            .set(self.submits.get().saturating_add(stats.submit_count));
395        self.command_encoder_count.set(
396            self.command_encoder_count
397                .get()
398                .saturating_add(stats.encoder_count),
399        );
400        self.command_submit_count.set(
401            self.command_submit_count
402                .get()
403                .saturating_add(stats.submit_count),
404        );
405        self.command_pass_count.set(
406            self.command_pass_count
407                .get()
408                .saturating_add(stats.pass_count),
409        );
410        self.command_transient_texture_bytes.set(
411            self.command_transient_texture_bytes
412                .get()
413                .saturating_add(stats.transient_texture_bytes),
414        );
415        self.command_retained_texture_bytes.set(
416            self.command_retained_texture_bytes
417                .get()
418                .max(stats.retained_texture_bytes),
419        );
420        self.command_upload_bytes.set(
421            self.command_upload_bytes
422                .get()
423                .saturating_add(stats.upload_bytes),
424        );
425    }
426
427    pub fn record_offscreen_acquire(&self, width: u32, height: u32, is_new: bool) {
428        self.offscreen_acquires
429            .set(self.offscreen_acquires.get() + 1);
430        if is_new {
431            self.offscreen_news.set(self.offscreen_news.get() + 1);
432        }
433        self.offscreen_total_bytes
434            .set(self.offscreen_total_bytes.get() + (width as u64) * (height as u64) * 4);
435    }
436
437    pub fn record_upload_bytes(&self, bytes: u64) {
438        self.upload_bytes
439            .set(self.upload_bytes.get().saturating_add(bytes));
440    }
441
442    pub fn record_isolated_layer_render(
443        &self,
444        width: u32,
445        height: u32,
446        node_id: Option<NodeId>,
447        logical_rect: Rect,
448        reasons: LayerSurfaceReasons,
449    ) {
450        self.isolated_layer_renders
451            .set(self.isolated_layer_renders.get().saturating_add(1));
452        self.isolated_layer_pixels.set(
453            self.isolated_layer_pixels
454                .get()
455                .saturating_add((width as u64) * (height as u64)),
456        );
457        self.record_top_isolated_layer(IsolatedLayerStat {
458            node_id,
459            logical_rect,
460            width,
461            height,
462            reasons,
463        });
464    }
465
466    pub fn record_layer_cache_hit(&self, width: u32, height: u32) {
467        self.layer_cache_hits
468            .set(self.layer_cache_hits.get().saturating_add(1));
469        self.layer_cache_hit_pixels.set(
470            self.layer_cache_hit_pixels
471                .get()
472                .saturating_add((width as u64) * (height as u64)),
473        );
474    }
475
476    pub fn record_layer_cache_miss(&self, width: u32, height: u32) {
477        self.layer_cache_misses
478            .set(self.layer_cache_misses.get().saturating_add(1));
479        self.layer_cache_miss_pixels.set(
480            self.layer_cache_miss_pixels
481                .get()
482                .saturating_add((width as u64) * (height as u64)),
483        );
484    }
485
486    pub fn record_layer_cache_eviction(&self) {
487        self.layer_cache_evictions
488            .set(self.layer_cache_evictions.get().saturating_add(1));
489    }
490
491    pub fn record_shadow_shape_cache_hit(&self, width: u32, height: u32) {
492        self.shadow_shape_cache_hits
493            .set(self.shadow_shape_cache_hits.get().saturating_add(1));
494        self.shadow_shape_cache_hit_pixels.set(
495            self.shadow_shape_cache_hit_pixels
496                .get()
497                .saturating_add((width as u64) * (height as u64)),
498        );
499    }
500
501    pub fn record_shadow_shape_cache_miss(&self, width: u32, height: u32) {
502        self.shadow_shape_cache_misses
503            .set(self.shadow_shape_cache_misses.get().saturating_add(1));
504        self.shadow_shape_cache_miss_pixels.set(
505            self.shadow_shape_cache_miss_pixels
506                .get()
507                .saturating_add((width as u64) * (height as u64)),
508        );
509    }
510
511    #[allow(clippy::too_many_arguments)]
512    pub fn maybe_print_shadow_shape_cache_miss(
513        &self,
514        width: u32,
515        height: u32,
516        content_hash: u64,
517        blur_radius: f32,
518        viewport_offset: [f32; 2],
519        shape_count: usize,
520        clip: Option<Rect>,
521    ) {
522        if !shadow_cache_diagnostics_enabled() {
523            return;
524        }
525
526        let count = self.shadow_shape_cache_miss_log_count.get();
527        if count >= 16 {
528            return;
529        }
530        self.shadow_shape_cache_miss_log_count.set(count + 1);
531
532        let clip_text = clip
533            .map(|clip| {
534                format!(
535                    "({:.1},{:.1},{:.1},{:.1})",
536                    clip.x, clip.y, clip.width, clip.height
537                )
538            })
539            .unwrap_or_else(|| "none".to_string());
540        eprintln!(
541            "[shadow-cache-miss #{count}] size={}x{} content_hash={content_hash} blur={:.2} viewport_offset=({:.1},{:.1}) shapes={} clip={}",
542            width,
543            height,
544            blur_radius,
545            viewport_offset[0],
546            viewport_offset[1],
547            shape_count,
548            clip_text,
549        );
550    }
551
552    pub fn record_shadow_text_blur_fallback(&self) {
553        self.shadow_text_blur_fallbacks
554            .set(self.shadow_text_blur_fallbacks.get().saturating_add(1));
555    }
556
557    pub fn bump_shapes(&self) {
558        self.shape_passes.set(self.shape_passes.get() + 1);
559    }
560
561    pub fn bump_images(&self) {
562        self.image_passes.set(self.image_passes.get() + 1);
563    }
564
565    pub fn bump_text(&self) {
566        self.text_passes.set(self.text_passes.get() + 1);
567    }
568
569    pub fn record_text_image_cache_hit(&self, width: u32, height: u32) {
570        self.text_image_cache_hits
571            .set(self.text_image_cache_hits.get().saturating_add(1));
572        self.text_image_cache_hit_pixels.set(
573            self.text_image_cache_hit_pixels
574                .get()
575                .saturating_add((width as u64) * (height as u64)),
576        );
577    }
578
579    pub fn record_text_image_cache_miss(&self, width: u32, height: u32) {
580        let pixels = (width as u64) * (height as u64);
581        self.text_image_cache_misses
582            .set(self.text_image_cache_misses.get().saturating_add(1));
583        self.text_image_cache_miss_pixels.set(
584            self.text_image_cache_miss_pixels
585                .get()
586                .saturating_add(pixels),
587        );
588        self.text_image_raster_bytes.set(
589            self.text_image_raster_bytes
590                .get()
591                .saturating_add(pixels * 4),
592        );
593    }
594
595    pub fn record_text_glyph_atlas_hit(&self) {
596        self.text_glyph_atlas_hits
597            .set(self.text_glyph_atlas_hits.get().saturating_add(1));
598    }
599
600    pub fn record_text_glyph_atlas_miss(&self, width: u32, height: u32) {
601        self.text_glyph_atlas_misses
602            .set(self.text_glyph_atlas_misses.get().saturating_add(1));
603        self.text_glyph_atlas_miss_pixels.set(
604            self.text_glyph_atlas_miss_pixels
605                .get()
606                .saturating_add((width as u64) * (height as u64)),
607        );
608    }
609
610    pub fn snapshot(&self) -> FrameStatsSnapshot {
611        let retained_texture_bytes = self
612            .offscreen_pool_bytes
613            .get()
614            .saturating_add(self.layer_cache_bytes.get());
615        FrameStatsSnapshot {
616            submits: self.submits.get(),
617            encoder_count: self.command_encoder_count.get(),
618            submit_count: self.command_submit_count.get(),
619            pass_count: self.command_pass_count.get(),
620            offscreen_acquires: self.offscreen_acquires.get(),
621            offscreen_news: self.offscreen_news.get(),
622            offscreen_total_bytes: self.offscreen_total_bytes.get(),
623            transient_texture_bytes: self
624                .offscreen_total_bytes
625                .get()
626                .saturating_add(self.command_transient_texture_bytes.get()),
627            retained_texture_bytes: retained_texture_bytes
628                .saturating_add(self.command_retained_texture_bytes.get()),
629            upload_bytes: self
630                .upload_bytes
631                .get()
632                .saturating_add(self.command_upload_bytes.get()),
633            isolated_layer_renders: self.isolated_layer_renders.get(),
634            isolated_layer_pixels: self.isolated_layer_pixels.get(),
635            layer_cache_hits: self.layer_cache_hits.get(),
636            layer_cache_misses: self.layer_cache_misses.get(),
637            layer_cache_evictions: self.layer_cache_evictions.get(),
638            layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
639            layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
640            shadow_shape_cache_hits: self.shadow_shape_cache_hits.get(),
641            shadow_shape_cache_misses: self.shadow_shape_cache_misses.get(),
642            shadow_shape_cache_hit_pixels: self.shadow_shape_cache_hit_pixels.get(),
643            shadow_shape_cache_miss_pixels: self.shadow_shape_cache_miss_pixels.get(),
644            shadow_text_blur_fallbacks: self.shadow_text_blur_fallbacks.get(),
645            blur_passes: self.blur_passes.get(),
646            composite_passes: self.composite_passes.get(),
647            effect_applies: self.effect_applies.get(),
648            shape_passes: self.shape_passes.get(),
649            image_passes: self.image_passes.get(),
650            text_passes: self.text_passes.get(),
651            text_image_cache_hits: self.text_image_cache_hits.get(),
652            text_image_cache_misses: self.text_image_cache_misses.get(),
653            text_image_cache_hit_pixels: self.text_image_cache_hit_pixels.get(),
654            text_image_cache_miss_pixels: self.text_image_cache_miss_pixels.get(),
655            text_image_raster_bytes: self.text_image_raster_bytes.get(),
656            text_glyph_atlas_hits: self.text_glyph_atlas_hits.get(),
657            text_glyph_atlas_misses: self.text_glyph_atlas_misses.get(),
658            text_glyph_atlas_miss_pixels: self.text_glyph_atlas_miss_pixels.get(),
659            offscreen_pool_size: self.offscreen_pool_size.get(),
660            offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
661            text_pool_size: self.text_pool_size.get(),
662            layer_cache_size: self.layer_cache_size.get(),
663            layer_cache_bytes: self.layer_cache_bytes.get(),
664            image_cache_size: self.image_cache_size.get(),
665            text_cache_size: self.text_cache_size.get(),
666            top_isolated_layers: *self.top_isolated_layers.borrow(),
667            top_isolated_layer_count: self.top_isolated_layer_count.get(),
668        }
669    }
670
671    pub fn reset(&self) {
672        self.submits.set(0);
673        self.command_encoder_count.set(0);
674        self.command_submit_count.set(0);
675        self.command_pass_count.set(0);
676        self.command_transient_texture_bytes.set(0);
677        self.command_retained_texture_bytes.set(0);
678        self.command_upload_bytes.set(0);
679        self.offscreen_acquires.set(0);
680        self.offscreen_news.set(0);
681        self.offscreen_total_bytes.set(0);
682        self.upload_bytes.set(0);
683        self.isolated_layer_renders.set(0);
684        self.isolated_layer_pixels.set(0);
685        self.layer_cache_hits.set(0);
686        self.layer_cache_misses.set(0);
687        self.layer_cache_evictions.set(0);
688        self.layer_cache_hit_pixels.set(0);
689        self.layer_cache_miss_pixels.set(0);
690        self.shadow_shape_cache_hits.set(0);
691        self.shadow_shape_cache_misses.set(0);
692        self.shadow_shape_cache_hit_pixels.set(0);
693        self.shadow_shape_cache_miss_pixels.set(0);
694        self.shadow_text_blur_fallbacks.set(0);
695        self.blur_passes.set(0);
696        self.composite_passes.set(0);
697        self.effect_applies.set(0);
698        self.shape_passes.set(0);
699        self.image_passes.set(0);
700        self.text_passes.set(0);
701        self.text_image_cache_hits.set(0);
702        self.text_image_cache_misses.set(0);
703        self.text_image_cache_hit_pixels.set(0);
704        self.text_image_cache_miss_pixels.set(0);
705        self.text_image_raster_bytes.set(0);
706        self.text_glyph_atlas_hits.set(0);
707        self.text_glyph_atlas_misses.set(0);
708        self.text_glyph_atlas_miss_pixels.set(0);
709        *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
710        self.top_isolated_layer_count.set(0);
711        self.shadow_shape_cache_miss_log_count.set(0);
712    }
713
714    pub fn maybe_print_snapshot(
715        &self,
716        snapshot: FrameStatsSnapshot,
717        frame_count: &mut u64,
718        enabled: bool,
719    ) {
720        if !enabled {
721            return;
722        }
723        *frame_count += 1;
724        if (*frame_count).is_multiple_of(60) {
725            snapshot.print(*frame_count);
726        }
727    }
728
729    fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
730        if !layer.reasons.has_any() {
731            return;
732        }
733
734        let mut top_layers = self.top_isolated_layers.borrow_mut();
735        let len = self.top_isolated_layer_count.get();
736        let insert_at = top_layers[..len]
737            .iter()
738            .enumerate()
739            .find_map(|(index, existing)| {
740                existing
741                    .filter(|existing| layer.pixel_area() > existing.pixel_area())
742                    .map(|_| index)
743            })
744            .unwrap_or(len);
745
746        if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
747            return;
748        }
749
750        let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
751            len + 1
752        } else {
753            TOP_ISOLATED_LAYER_LIMIT
754        };
755
756        let mut index = new_len.saturating_sub(1);
757        while index > insert_at {
758            top_layers[index] = top_layers[index - 1];
759            index -= 1;
760        }
761        top_layers[insert_at] = Some(layer);
762        self.top_isolated_layer_count.set(new_len);
763    }
764}
765
766pub(crate) fn gpu_stats_enabled() -> bool {
767    std::env::var("CRANPOSE_GPU_STATS")
768        .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
769        .unwrap_or(false)
770}
771
772fn shadow_cache_diagnostics_enabled() -> bool {
773    std::env::var("CRANPOSE_GPU_SHADOW_CACHE_DIAG")
774        .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
775        .unwrap_or(false)
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    #[test]
783    fn layer_cache_counters_accumulate_and_reset() {
784        let stats = FrameStats::default();
785        stats.record_upload_bytes(512);
786        stats.record_command_stats(FrameCommandStats {
787            encoder_count: 1,
788            submit_count: 1,
789            pass_count: 2,
790            transient_texture_bytes: 256,
791            retained_texture_bytes: 128,
792            upload_bytes: 64,
793        });
794        stats.bump_shapes();
795        stats.blur_passes.set(1);
796        stats.offscreen_total_bytes.set(1024);
797        stats.offscreen_pool_bytes.set(2048);
798        stats.record_layer_cache_hit(10, 20);
799        stats.record_layer_cache_hit(3, 4);
800        stats.record_layer_cache_miss(5, 6);
801        stats.record_layer_cache_eviction();
802        stats.record_shadow_shape_cache_hit(8, 9);
803        stats.record_shadow_shape_cache_miss(10, 11);
804        stats.record_shadow_text_blur_fallback();
805        stats.record_text_image_cache_hit(13, 17);
806        stats.record_text_image_cache_miss(19, 23);
807
808        assert_eq!(stats.layer_cache_hits.get(), 2);
809        assert_eq!(stats.layer_cache_misses.get(), 1);
810        assert_eq!(stats.layer_cache_evictions.get(), 1);
811        assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
812        assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
813        assert_eq!(stats.shadow_shape_cache_hits.get(), 1);
814        assert_eq!(stats.shadow_shape_cache_misses.get(), 1);
815        assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 72);
816        assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 110);
817        assert_eq!(stats.shadow_text_blur_fallbacks.get(), 1);
818
819        stats.record_isolated_layer_render(
820            7,
821            8,
822            Some(9),
823            Rect {
824                x: 2.0,
825                y: 3.0,
826                width: 4.0,
827                height: 5.0,
828            },
829            LayerSurfaceReasons {
830                text_local_surface: true,
831                ..LayerSurfaceReasons::default()
832            },
833        );
834        let snapshot = stats.snapshot();
835
836        assert_eq!(snapshot.isolated_layer_renders, 1);
837        assert_eq!(snapshot.isolated_layer_pixels, 56);
838        assert_eq!(snapshot.upload_bytes, 576);
839        assert_eq!(snapshot.encoder_count, 1);
840        assert_eq!(snapshot.submit_count, 1);
841        assert_eq!(snapshot.pass_count, 2);
842        assert_eq!(snapshot.transient_texture_bytes, 1280);
843        assert_eq!(snapshot.retained_texture_bytes, 2176);
844        assert_eq!(snapshot.layer_cache_hits, 2);
845        assert_eq!(snapshot.layer_cache_misses, 1);
846        assert_eq!(snapshot.shadow_shape_cache_hits, 1);
847        assert_eq!(snapshot.shadow_shape_cache_misses, 1);
848        assert_eq!(snapshot.shadow_shape_cache_hit_pixels, 72);
849        assert_eq!(snapshot.shadow_shape_cache_miss_pixels, 110);
850        assert_eq!(snapshot.shadow_text_blur_fallbacks, 1);
851        assert_eq!(snapshot.text_image_cache_hits, 1);
852        assert_eq!(snapshot.text_image_cache_misses, 1);
853        assert_eq!(snapshot.text_image_cache_hit_pixels, 221);
854        assert_eq!(snapshot.text_image_cache_miss_pixels, 437);
855        assert_eq!(snapshot.text_image_raster_bytes, 1748);
856        let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
857        assert_eq!(top_layers.len(), 1);
858        assert_eq!(top_layers[0].node_id, Some(9));
859        assert_eq!(stats.layer_cache_hits.get(), 2);
860        assert_eq!(stats.layer_cache_misses.get(), 1);
861
862        stats.reset();
863
864        assert_eq!(stats.layer_cache_hits.get(), 0);
865        assert_eq!(stats.layer_cache_misses.get(), 0);
866        assert_eq!(stats.layer_cache_evictions.get(), 0);
867        assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
868        assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
869        assert_eq!(stats.shadow_shape_cache_hits.get(), 0);
870        assert_eq!(stats.shadow_shape_cache_misses.get(), 0);
871        assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 0);
872        assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 0);
873        assert_eq!(stats.shadow_text_blur_fallbacks.get(), 0);
874        assert_eq!(stats.text_image_cache_hits.get(), 0);
875        assert_eq!(stats.text_image_cache_misses.get(), 0);
876        assert_eq!(stats.text_image_cache_hit_pixels.get(), 0);
877        assert_eq!(stats.text_image_cache_miss_pixels.get(), 0);
878        assert_eq!(stats.text_image_raster_bytes.get(), 0);
879        assert_eq!(stats.upload_bytes.get(), 0);
880        assert_eq!(stats.isolated_layer_renders.get(), 0);
881        assert_eq!(stats.isolated_layer_pixels.get(), 0);
882        assert_eq!(stats.top_isolated_layer_count.get(), 0);
883    }
884
885    #[test]
886    fn command_stats_accumulate_and_reset() {
887        let stats = FrameStats::default();
888
889        stats.record_command_stats(FrameCommandStats {
890            encoder_count: 2,
891            submit_count: 2,
892            pass_count: 5,
893            transient_texture_bytes: 1024,
894            retained_texture_bytes: 2048,
895            upload_bytes: 512,
896        });
897        stats.bump_shapes();
898
899        let snapshot = stats.snapshot();
900        assert_eq!(snapshot.submits, 2);
901        assert_eq!(snapshot.encoder_count, 2);
902        assert_eq!(snapshot.submit_count, 2);
903        assert_eq!(snapshot.pass_count, 5);
904        assert_eq!(snapshot.transient_texture_bytes, 1024);
905        assert_eq!(snapshot.retained_texture_bytes, 2048);
906        assert_eq!(snapshot.upload_bytes, 512);
907
908        stats.reset();
909        let reset = stats.snapshot();
910        assert_eq!(reset.submits, 0);
911        assert_eq!(reset.encoder_count, 0);
912        assert_eq!(reset.submit_count, 0);
913        assert_eq!(reset.pass_count, 0);
914        assert_eq!(reset.transient_texture_bytes, 0);
915        assert_eq!(reset.retained_texture_bytes, 0);
916        assert_eq!(reset.upload_bytes, 0);
917    }
918
919    #[test]
920    fn snapshot_adds_explicit_readback_command_stats() {
921        let stats = FrameStats::default();
922        stats.record_command_stats(FrameCommandStats {
923            encoder_count: 1,
924            submit_count: 1,
925            pass_count: 2,
926            transient_texture_bytes: 128,
927            retained_texture_bytes: 512,
928            upload_bytes: 64,
929        });
930        let snapshot = stats
931            .snapshot()
932            .with_command_stats_added(FrameCommandStats {
933                encoder_count: 1,
934                submit_count: 1,
935                pass_count: 1,
936                transient_texture_bytes: 0,
937                retained_texture_bytes: 0,
938                upload_bytes: 0,
939            });
940
941        assert_eq!(snapshot.submits, 2);
942        assert_eq!(snapshot.encoder_count, 2);
943        assert_eq!(snapshot.submit_count, 2);
944        assert_eq!(snapshot.pass_count, 3);
945        assert_eq!(snapshot.transient_texture_bytes, 128);
946        assert_eq!(snapshot.retained_texture_bytes, 512);
947        assert_eq!(snapshot.upload_bytes, 64);
948    }
949
950    #[test]
951    fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
952        let stats = FrameStats::default();
953        let snapshot = stats.snapshot();
954        let mut frame_count = 0;
955
956        stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
957        assert_eq!(frame_count, 0);
958
959        stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
960        assert_eq!(frame_count, 1);
961    }
962
963    #[test]
964    fn layer_surface_reasons_report_runtime_only_bits() {
965        let reasons = LayerSurfaceReasons {
966            immediate_shadow: true,
967            text_local_surface: true,
968            mixed_direct_content: true,
969            ..LayerSurfaceReasons::default()
970        };
971
972        assert!(reasons.has_any());
973        assert!(reasons.has_renderer_forced_surface());
974        assert_eq!(
975            reasons.labels().collect::<Vec<_>>(),
976            vec![
977                "immediate_shadow",
978                "text_local_surface",
979                "mixed_direct_content"
980            ]
981        );
982        assert_eq!(
983            reasons.display(),
984            "immediate_shadow+text_local_surface+mixed_direct_content"
985        );
986    }
987
988    #[test]
989    fn immediate_shadow_only_is_diagnostic_not_isolating() {
990        let reasons = LayerSurfaceReasons {
991            immediate_shadow: true,
992            ..LayerSurfaceReasons::default()
993        };
994
995        assert!(!reasons.has_any());
996        assert!(!reasons.has_renderer_forced_surface());
997        assert_eq!(
998            reasons.labels().collect::<Vec<_>>(),
999            vec!["immediate_shadow"]
1000        );
1001        assert_eq!(reasons.display(), "immediate_shadow");
1002    }
1003
1004    #[test]
1005    fn mixed_direct_content_only_is_diagnostic_not_isolating() {
1006        let reasons = LayerSurfaceReasons {
1007            mixed_direct_content: true,
1008            ..LayerSurfaceReasons::default()
1009        };
1010
1011        assert!(!reasons.has_any());
1012        assert!(!reasons.has_renderer_forced_surface());
1013        assert_eq!(
1014            reasons.labels().collect::<Vec<_>>(),
1015            vec!["mixed_direct_content"]
1016        );
1017        assert_eq!(reasons.display(), "mixed_direct_content");
1018    }
1019
1020    #[test]
1021    fn has_any_matches_has_isolating_requirement_for_each_requirement() {
1022        let all_requirements = [
1023            SurfaceRequirement::ExplicitOffscreen,
1024            SurfaceRequirement::RenderEffect,
1025            SurfaceRequirement::Backdrop,
1026            SurfaceRequirement::GroupOpacity,
1027            SurfaceRequirement::BlendMode,
1028            SurfaceRequirement::ShapeClip,
1029            SurfaceRequirement::ImmediateShadow,
1030            SurfaceRequirement::TextMaterialMask,
1031            SurfaceRequirement::MotionStableCapture,
1032            SurfaceRequirement::NonTranslationTransform,
1033            SurfaceRequirement::MixedDirectContent,
1034            SurfaceRequirement::PixelStableComposite,
1035        ];
1036        for requirement in all_requirements {
1037            let set = SurfaceRequirementSet::default().with(requirement);
1038            let reasons = LayerSurfaceReasons::from(set);
1039            assert_eq!(
1040                reasons.has_any(),
1041                set.has_isolating_requirement(),
1042                "has_any vs has_isolating_requirement mismatch for {requirement:?}"
1043            );
1044        }
1045    }
1046
1047    #[test]
1048    fn top_isolated_layers_keep_largest_runtime_surfaces() {
1049        let stats = FrameStats::default();
1050        for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
1051            stats.record_isolated_layer_render(
1052                16 + index as u32,
1053                8 + index as u32,
1054                Some(index),
1055                Rect {
1056                    x: index as f32,
1057                    y: 0.0,
1058                    width: 10.0,
1059                    height: 10.0,
1060                },
1061                LayerSurfaceReasons {
1062                    text_local_surface: true,
1063                    ..LayerSurfaceReasons::default()
1064                },
1065            );
1066        }
1067
1068        let snapshot = stats.snapshot();
1069        let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
1070        assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
1071        assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
1072        assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
1073    }
1074}