Skip to main content

viewport_lib/renderer/
stats.rs

1//! Per-frame performance counters for the viewport renderer.
2
3/// Controls the renderer's internal default behavior.
4///
5/// The host application owns playback state, time, and scene content.
6/// `RuntimeMode` tells the renderer what workload to expect so it can adjust
7/// internal defaults (e.g. picking rate) accordingly.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum RuntimeMode {
10    /// Prioritize responsiveness and picking accuracy.
11    #[default]
12    Interactive,
13    /// Prioritize steady frame pacing; picking may be throttled.
14    Playback,
15    /// Restore full quality; picking runs at full rate.
16    Paused,
17    /// Deterministic full-quality render intended for screenshot or export.
18    Capture,
19}
20
21/// Controls what quality reductions the viewport is allowed to apply under load.
22///
23/// Set once via [`crate::ViewportRenderer::set_performance_policy`]. The internal
24/// adaptation controller reads `target_fps` and adjusts render scale within
25/// `[min_render_scale, max_render_scale]` when `allow_dynamic_resolution` is true.
26///
27/// Pass-specific flags (`allow_shadow_reduction`, `allow_volume_quality_reduction`,
28/// `allow_effect_throttling`) gate concrete quality reductions that kick in when
29/// the previous frame missed the target budget.
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct PerformancePolicy {
32    /// Target frames per second. `None` means uncapped; `missed_budget` is always `false`.
33    pub target_fps: Option<f32>,
34    /// Lower bound for dynamic render scale (e.g. 0.5 = half resolution).
35    pub min_render_scale: f32,
36    /// Upper bound for dynamic render scale (1.0 = native).
37    pub max_render_scale: f32,
38    /// Allow the viewport to adjust render scale automatically when budget is exceeded.
39    ///
40    /// When `false`, the internal controller is inactive and render scale can be
41    /// set manually via [`crate::ViewportRenderer::set_render_scale`].
42    pub allow_dynamic_resolution: bool,
43    /// Allow the viewport to skip the shadow pass under load.
44    ///
45    /// When `true` and the previous frame exceeded the target budget, the shadow depth
46    /// pass is skipped entirely. Shadows reappear as soon as the frame is within budget.
47    pub allow_shadow_reduction: bool,
48    /// Allow the viewport to reduce volume raymarch quality under load.
49    ///
50    /// When `true` and the previous frame exceeded the target budget, the per-volume
51    /// step size is doubled (half the number of samples), reducing GPU cost at the
52    /// cost of coarser volume appearance.
53    pub allow_volume_quality_reduction: bool,
54    /// Allow the viewport to skip non-essential HDR effect passes under load.
55    ///
56    /// When `true` and the previous frame exceeded the target budget, the SSAO,
57    /// contact shadow, and bloom passes are skipped for that frame.
58    pub allow_effect_throttling: bool,
59}
60
61impl Default for PerformancePolicy {
62    fn default() -> Self {
63        Self {
64            target_fps: None,
65            min_render_scale: 0.5,
66            max_render_scale: 1.0,
67            allow_dynamic_resolution: false,
68            allow_shadow_reduction: false,
69            allow_volume_quality_reduction: false,
70            allow_effect_throttling: false,
71        }
72    }
73}
74
75/// Per-frame rendering statistics returned by [`crate::ViewportRenderer::prepare`].
76#[derive(Debug, Clone, Copy, Default)]
77pub struct FrameStats {
78    /// Total objects considered for rendering.
79    pub total_objects: u32,
80    /// Objects that passed visibility and frustum tests.
81    pub visible_objects: u32,
82    /// Objects culled by frustum or visibility.
83    pub culled_objects: u32,
84    /// Number of draw calls issued in the main pass.
85    pub draw_calls: u32,
86    /// Number of instanced batches (0 when using per-object path).
87    pub instanced_batches: u32,
88    /// Total triangles submitted to the GPU.
89    pub triangles_submitted: u64,
90    /// Number of draw calls in the shadow pass.
91    pub shadow_draw_calls: u32,
92    /// CPU time spent in `prepare()`, in milliseconds.
93    pub cpu_prepare_ms: f32,
94    /// GPU scene-pass time in milliseconds, if timestamp queries are available.
95    ///
96    /// Measured with `TIMESTAMP_QUERY` around the main scene render pass.
97    /// `None` on backends that do not support `TIMESTAMP_QUERY` (e.g. WebGL).
98    ///
99    /// Note: this value reflects the *previous* frame's GPU cost due to async
100    /// readback. The value lags by one frame and should not be used by the
101    /// adaptation controller across mode transitions.
102    pub gpu_frame_ms: Option<f32>,
103    /// Wall-clock duration since the previous `prepare()` call, in milliseconds.
104    ///
105    /// Approximates the full frame interval. Zero on the first frame.
106    pub total_frame_ms: f32,
107    /// Current internal render scale (1.0 = native resolution).
108    ///
109    /// Reflects the value tracked by the adaptation controller. Values below 1.0
110    /// cause the scene to render into a scaled intermediate texture that is
111    /// bilinearly upscaled to the surface (requires `allow_dynamic_resolution`).
112    pub render_scale: f32,
113    /// True if the last frame exceeded the target frame budget.
114    ///
115    /// Requires `target_fps` to be set in the [`PerformancePolicy`]. Always
116    /// `false` when no target is configured.
117    pub missed_budget: bool,
118    /// Bytes of geometry data uploaded to the GPU since the previous
119    /// `prepare()` call.
120    ///
121    /// Counts full buffer reallocations triggered by
122    /// [`crate::ViewportGpuResources::replace_mesh_data`] and initial uploads
123    /// via `upload_mesh_data` / `upload_mesh`. Uniform buffer writes are not
124    /// counted.
125    pub upload_bytes: u64,
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_frame_stats_default_is_zero() {
134        let stats = FrameStats::default();
135        assert_eq!(stats.total_objects, 0);
136        assert_eq!(stats.visible_objects, 0);
137        assert_eq!(stats.culled_objects, 0);
138        assert_eq!(stats.draw_calls, 0);
139        assert_eq!(stats.instanced_batches, 0);
140        assert_eq!(stats.triangles_submitted, 0);
141        assert_eq!(stats.shadow_draw_calls, 0);
142        assert_eq!(stats.cpu_prepare_ms, 0.0);
143        assert!(stats.gpu_frame_ms.is_none());
144        assert_eq!(stats.total_frame_ms, 0.0);
145        assert_eq!(stats.render_scale, 0.0);
146        assert!(!stats.missed_budget);
147        assert_eq!(stats.upload_bytes, 0);
148    }
149
150    #[test]
151    fn test_runtime_mode_default_is_interactive() {
152        assert_eq!(RuntimeMode::default(), RuntimeMode::Interactive);
153    }
154
155    #[test]
156    fn test_performance_policy_default() {
157        let p = PerformancePolicy::default();
158        assert!(p.target_fps.is_none());
159        assert!(!p.allow_dynamic_resolution);
160        assert!(p.min_render_scale <= p.max_render_scale);
161        assert_eq!(p.max_render_scale, 1.0);
162    }
163}