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 /// True when GPU-driven culling is active this frame.
127 ///
128 /// False when the device does not support `INDIRECT_FIRST_INSTANCE` or
129 /// culling has been disabled via `disable_gpu_driven_culling()`.
130 pub gpu_culling_active: bool,
131 /// Number of instances that passed GPU culling and were submitted for drawing.
132 ///
133 /// Populated only when `gpu_culling_active` is true. `None` on the first frame
134 /// or when GPU culling is off. Lags by one frame due to async readback.
135 pub gpu_visible_instances: Option<u32>,
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_frame_stats_default_is_zero() {
144 let stats = FrameStats::default();
145 assert_eq!(stats.total_objects, 0);
146 assert_eq!(stats.visible_objects, 0);
147 assert_eq!(stats.culled_objects, 0);
148 assert_eq!(stats.draw_calls, 0);
149 assert_eq!(stats.instanced_batches, 0);
150 assert_eq!(stats.triangles_submitted, 0);
151 assert_eq!(stats.shadow_draw_calls, 0);
152 assert_eq!(stats.cpu_prepare_ms, 0.0);
153 assert!(stats.gpu_frame_ms.is_none());
154 assert_eq!(stats.total_frame_ms, 0.0);
155 assert_eq!(stats.render_scale, 0.0);
156 assert!(!stats.missed_budget);
157 assert_eq!(stats.upload_bytes, 0);
158 assert!(!stats.gpu_culling_active);
159 assert!(stats.gpu_visible_instances.is_none());
160 }
161
162 #[test]
163 fn test_runtime_mode_default_is_interactive() {
164 assert_eq!(RuntimeMode::default(), RuntimeMode::Interactive);
165 }
166
167 #[test]
168 fn test_performance_policy_default() {
169 let p = PerformancePolicy::default();
170 assert!(p.target_fps.is_none());
171 assert!(!p.allow_dynamic_resolution);
172 assert!(p.min_render_scale <= p.max_render_scale);
173 assert_eq!(p.max_render_scale, 1.0);
174 }
175}