Skip to main content

alpha2_render/
diagnostics.rs

1//! Stable, host-readable renderer diagnostics.
2
3use std::collections::VecDeque;
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use alpha2_gpu::{Capabilities, CapabilityTier};
8
9use crate::{
10    PipelineCacheStats, ReadbackBufferPoolStats, TexturePoolStats, UniformBufferPoolStats,
11    UploadTexturePoolStats,
12};
13
14/// Source of one pass duration.
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16pub enum TimingSource {
17    /// Hardware timestamp query resolved by wgpu.
18    GpuTimestamp,
19    /// Wall-clock time spent encoding one pass on the CPU.
20    CpuEncoding,
21}
22
23/// Most recent timing for one named render pass.
24///
25/// Pass timing is opt-in; see [`crate::Renderer::set_pass_timing_enabled`].
26#[derive(Copy, Clone, Debug, Eq, PartialEq)]
27pub struct PassTiming {
28    /// Stable pass label.
29    pub label: &'static str,
30    /// Duration in nanoseconds.
31    pub nanoseconds: u64,
32    /// Measurement source.
33    pub source: TimingSource,
34}
35
36/// Adapter metadata recorded beside reproducible benchmark samples.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct AdapterDiagnostics {
39    /// Platform adapter name.
40    pub name: String,
41    /// Selected graphics backend.
42    pub backend: String,
43    /// Driver name.
44    pub driver: String,
45    /// Driver version/details.
46    pub driver_info: String,
47    /// Capability tier driving deterministic degradation.
48    pub tier: CapabilityTier,
49    /// Whether hardware timestamps are active.
50    pub timestamp_queries: bool,
51}
52
53/// Snapshot of render performance, cache, memory, and scheduling behavior.
54#[derive(Clone, Debug, PartialEq)]
55pub struct RenderDiagnostics {
56    /// Reproducibility metadata.
57    pub adapter: AdapterDiagnostics,
58    /// Latest bounded pass timing history. Empty unless pass timing is enabled.
59    pub passes: Vec<PassTiming>,
60    /// Intermediate texture allocation/reuse counters.
61    pub textures: TexturePoolStats,
62    /// Source-upload texture allocation/reuse and live-byte counters.
63    pub upload_textures: UploadTexturePoolStats,
64    /// Mapped readback-buffer allocation/reuse and live-byte counters.
65    pub readback_buffers: ReadbackBufferPoolStats,
66    /// Submission uniform-arena allocation/reuse and live-byte counters.
67    pub uniform_buffers: UniformBufferPoolStats,
68    /// Runtime and built-in pipeline cache counts.
69    pub pipelines: PipelineCacheStats,
70    /// Kept graph results: budget, occupancy, hits, misses, invalidations.
71    pub result_cache: crate::ResultCacheStats,
72    /// Frames presented by the caller.
73    pub presented_frames: u64,
74    /// Requests that could not render a new frame.
75    pub dropped_frames: u64,
76    /// Presentations that held a previous frame.
77    pub repeated_frames: u64,
78    /// Accumulated host decoder wait time.
79    pub decoder_wait: Duration,
80    /// Active maximum filter samples: what the passes take, which may be
81    /// less than the caller asked for (see [`Self::degradations`]).
82    pub max_filter_samples: u32,
83    /// Active temporal subframe count.
84    pub motion_blur_subframes: u32,
85    /// Fallbacks applied to what the caller asked for: a filter sample
86    /// budget reduced to what the passes take.
87    pub degradations: Vec<crate::CapabilityDiagnostic>,
88    /// Non-source nodes encoded by completed graph executions.
89    pub graph_nodes: u64,
90    /// Actual render passes encoded by completed graph executions.
91    pub graph_passes: u64,
92    /// Queue submissions issued by completed graph executions.
93    pub graph_submissions: u64,
94    /// Queue submissions issued by source presentation/conversion.
95    pub source_submissions: u64,
96    /// Source passes that converted a decoded picture into the working
97    /// color space: YUV to RGB, or an RGBA presentation.
98    pub source_conversions: u64,
99    /// Decoded 4:2:0 pictures copied plane for plane into NV12 targets, with
100    /// no color conversion and no graph.
101    pub yuv_repacks: u64,
102    /// Queue submissions issued by output packing and readback.
103    pub readback_submissions: u64,
104    /// Queue submissions combining graph execution with output readback.
105    pub frame_submissions: u64,
106    /// Actual render passes across graph, source, and readback work.
107    pub render_passes: u64,
108    /// GPU copy commands encoded for output readback.
109    pub copy_operations: u64,
110    /// Logical texel bytes copied by output readback commands.
111    pub copy_bytes: u64,
112}
113
114/// Reproducible metadata envelope written beside benchmark measurements.
115#[derive(Clone, Debug, PartialEq)]
116pub struct BenchmarkMetadata {
117    /// Caller-defined project or fixture identity.
118    pub project: String,
119    /// Version of the alpha2 render crate.
120    pub alpha2_version: &'static str,
121    /// Adapter and driver identity.
122    pub adapter: AdapterDiagnostics,
123    /// Output width and height.
124    pub dimensions: (u32, u32),
125    /// Output scale used by the caller.
126    pub resolution_scale: f32,
127    /// Filter sample budget.
128    pub max_filter_samples: u32,
129    /// Temporal sample count.
130    pub motion_blur_subframes: u32,
131    /// Texture byte budget.
132    pub texture_budget_bytes: u64,
133}
134
135/// Entries retained while pass timing is enabled.
136const PASS_HISTORY: usize = 256;
137
138#[derive(Debug, Default)]
139pub(crate) struct DiagnosticsState {
140    passes: VecDeque<PassTiming>,
141    /// Which run of pass timing a record belongs to: advanced each time
142    /// timing is turned off, so a GPU timestamp read that lands later is
143    /// dropped instead of recorded into a history timing no longer keeps.
144    timing_epoch: u64,
145    presented_frames: u64,
146    dropped_frames: u64,
147    repeated_frames: u64,
148    decoder_wait: Duration,
149    graph_nodes: u64,
150    graph_passes: u64,
151    graph_submissions: u64,
152    source_submissions: u64,
153    source_conversions: u64,
154    yuv_repacks: u64,
155    readback_submissions: u64,
156    frame_submissions: u64,
157    render_passes: u64,
158    copy_operations: u64,
159    copy_bytes: u64,
160}
161
162pub(crate) type SharedDiagnostics = Arc<Mutex<DiagnosticsState>>;
163
164#[derive(Copy, Clone)]
165pub(crate) struct ResourceDiagnostics {
166    pub(crate) textures: TexturePoolStats,
167    pub(crate) upload_textures: UploadTexturePoolStats,
168    pub(crate) readback_buffers: ReadbackBufferPoolStats,
169    pub(crate) uniform_buffers: UniformBufferPoolStats,
170    pub(crate) pipelines: PipelineCacheStats,
171    pub(crate) result_cache: crate::ResultCacheStats,
172}
173
174/// The run of pass timing a pass encoded now belongs to (see
175/// [`crate::Renderer::pass_timing_epoch`]).
176pub(crate) fn timing_epoch(state: &SharedDiagnostics) -> u64 {
177    state
178        .lock()
179        .unwrap_or_else(std::sync::PoisonError::into_inner)
180        .timing_epoch
181}
182
183/// Records one pass duration, unless timing was turned off since the pass
184/// was encoded in timing run `epoch`. Callers take the epoch only while the
185/// renderer's opt-in flag is on, so this is never reached on the default
186/// path.
187pub(crate) fn record_pass(
188    state: &SharedDiagnostics,
189    epoch: u64,
190    label: &'static str,
191    duration: Duration,
192    source: TimingSource,
193) {
194    let mut state = state
195        .lock()
196        .unwrap_or_else(std::sync::PoisonError::into_inner);
197    if state.timing_epoch != epoch {
198        return;
199    }
200    if state.passes.len() == PASS_HISTORY {
201        state.passes.pop_front();
202    }
203    state.passes.push_back(PassTiming {
204        label,
205        nanoseconds: u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX),
206        source,
207    });
208}
209
210pub(crate) fn record_graph_work(
211    state: &SharedDiagnostics,
212    nodes: u64,
213    render_passes: u64,
214    standalone_submission: bool,
215) {
216    let mut state = state
217        .lock()
218        .unwrap_or_else(std::sync::PoisonError::into_inner);
219    state.graph_nodes = state.graph_nodes.saturating_add(nodes);
220    state.graph_passes = state.graph_passes.saturating_add(render_passes);
221    if standalone_submission {
222        state.graph_submissions = state.graph_submissions.saturating_add(1);
223    }
224    state.render_passes = state.render_passes.saturating_add(render_passes);
225}
226
227pub(crate) fn record_source_work(
228    state: &SharedDiagnostics,
229    render_passes: u64,
230    standalone_submission: bool,
231    conversion: bool,
232) {
233    let mut state = state
234        .lock()
235        .unwrap_or_else(std::sync::PoisonError::into_inner);
236    state.source_conversions = state
237        .source_conversions
238        .saturating_add(u64::from(conversion));
239    if standalone_submission {
240        state.source_submissions = state.source_submissions.saturating_add(1);
241    }
242    state.render_passes = state.render_passes.saturating_add(render_passes);
243}
244
245pub(crate) fn record_readback_work(
246    state: &SharedDiagnostics,
247    render_passes: u64,
248    copy_operations: u64,
249    copy_bytes: u64,
250    standalone_submission: bool,
251) {
252    let mut state = state
253        .lock()
254        .unwrap_or_else(std::sync::PoisonError::into_inner);
255    if standalone_submission {
256        state.readback_submissions = state.readback_submissions.saturating_add(1);
257    }
258    state.render_passes = state.render_passes.saturating_add(render_passes);
259    state.copy_operations = state.copy_operations.saturating_add(copy_operations);
260    state.copy_bytes = state.copy_bytes.saturating_add(copy_bytes);
261}
262
263pub(crate) fn record_frame_submission(state: &SharedDiagnostics) {
264    let mut state = state
265        .lock()
266        .unwrap_or_else(std::sync::PoisonError::into_inner);
267    state.frame_submissions = state.frame_submissions.saturating_add(1);
268}
269
270/// Drops retained pass history when timing is turned off, and ends the
271/// timing run, so records of passes encoded before arrive to nothing.
272pub(crate) fn end_timing_run(state: &SharedDiagnostics) {
273    let mut state = state
274        .lock()
275        .unwrap_or_else(std::sync::PoisonError::into_inner);
276    state.passes.clear();
277    state.timing_epoch = state.timing_epoch.wrapping_add(1);
278}
279
280pub(crate) fn snapshot(
281    state: &SharedDiagnostics,
282    capabilities: &Capabilities,
283    resources: &ResourceDiagnostics,
284    max_filter_samples: u32,
285    motion_blur_subframes: u32,
286    degradations: Vec<crate::CapabilityDiagnostic>,
287) -> RenderDiagnostics {
288    let state = state
289        .lock()
290        .unwrap_or_else(std::sync::PoisonError::into_inner);
291    RenderDiagnostics {
292        adapter: AdapterDiagnostics {
293            name: capabilities.adapter_name.clone(),
294            backend: capabilities.backend.clone(),
295            driver: capabilities.driver.clone(),
296            driver_info: capabilities.driver_info.clone(),
297            tier: capabilities.tier,
298            timestamp_queries: capabilities.timestamp_queries,
299        },
300        passes: state.passes.iter().copied().collect(),
301        textures: resources.textures,
302        upload_textures: resources.upload_textures,
303        readback_buffers: resources.readback_buffers,
304        uniform_buffers: resources.uniform_buffers,
305        pipelines: resources.pipelines,
306        result_cache: resources.result_cache,
307        presented_frames: state.presented_frames,
308        dropped_frames: state.dropped_frames,
309        repeated_frames: state.repeated_frames,
310        decoder_wait: state.decoder_wait,
311        max_filter_samples,
312        motion_blur_subframes,
313        degradations,
314        graph_nodes: state.graph_nodes,
315        graph_passes: state.graph_passes,
316        graph_submissions: state.graph_submissions,
317        source_submissions: state.source_submissions,
318        source_conversions: state.source_conversions,
319        yuv_repacks: state.yuv_repacks,
320        readback_submissions: state.readback_submissions,
321        frame_submissions: state.frame_submissions,
322        render_passes: state.render_passes,
323        copy_operations: state.copy_operations,
324        copy_bytes: state.copy_bytes,
325    }
326}
327
328pub(crate) fn record_yuv_repack(state: &SharedDiagnostics) {
329    let mut state = state
330        .lock()
331        .unwrap_or_else(std::sync::PoisonError::into_inner);
332    state.yuv_repacks = state.yuv_repacks.saturating_add(1);
333}
334
335pub(crate) fn record_frame(state: &SharedDiagnostics, presented: bool, repeated: bool) {
336    let mut state = state
337        .lock()
338        .unwrap_or_else(std::sync::PoisonError::into_inner);
339    state.presented_frames += u64::from(presented);
340    state.dropped_frames += u64::from(!presented);
341    state.repeated_frames += u64::from(repeated);
342}
343
344pub(crate) fn record_decoder_wait(state: &SharedDiagnostics, duration: Duration) {
345    let mut state = state
346        .lock()
347        .unwrap_or_else(std::sync::PoisonError::into_inner);
348    state.decoder_wait = state.decoder_wait.saturating_add(duration);
349}