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    /// The DX12 shader compiler; empty on other backends and in browsers.
52    pub shader_compiler: String,
53}
54
55/// Snapshot of render performance, cache, memory, and scheduling behavior.
56#[derive(Clone, Debug, PartialEq)]
57pub struct RenderDiagnostics {
58    /// Reproducibility metadata.
59    pub adapter: AdapterDiagnostics,
60    /// Latest bounded pass timing history. Empty unless pass timing is enabled.
61    pub passes: Vec<PassTiming>,
62    /// Intermediate texture allocation/reuse counters.
63    pub textures: TexturePoolStats,
64    /// Source-upload texture allocation/reuse and live-byte counters.
65    pub upload_textures: UploadTexturePoolStats,
66    /// Mapped readback-buffer allocation/reuse and live-byte counters.
67    pub readback_buffers: ReadbackBufferPoolStats,
68    /// Submission uniform-arena allocation/reuse and live-byte counters.
69    pub uniform_buffers: UniformBufferPoolStats,
70    /// Runtime and built-in pipeline cache counts.
71    pub pipelines: PipelineCacheStats,
72    /// Pipeline creations and their CPU time; a first draw that compiles shaders stalls its frame.
73    pub pipeline_creation: PipelineCreationStats,
74    /// Kept graph results: budget, occupancy, hits, misses, invalidations.
75    pub result_cache: crate::ResultCacheStats,
76    /// Frames presented by the caller.
77    pub presented_frames: u64,
78    /// Requests that could not render a new frame.
79    pub dropped_frames: u64,
80    /// Presentations that held a previous frame.
81    pub repeated_frames: u64,
82    /// Accumulated host decoder wait time.
83    pub decoder_wait: Duration,
84    /// Active filter sample budget, possibly below the request (see [`Self::degradations`]).
85    pub max_filter_samples: u32,
86    /// Active temporal subframe count.
87    pub motion_blur_subframes: u32,
88    /// Fallbacks applied to the caller's request, such as a reduced filter sample budget.
89    pub degradations: Vec<crate::CapabilityDiagnostic>,
90    /// Non-source nodes encoded by completed graph executions.
91    pub graph_nodes: u64,
92    /// Actual render passes encoded by completed graph executions.
93    pub graph_passes: u64,
94    /// Queue submissions issued by completed graph executions.
95    pub graph_submissions: u64,
96    /// Queue submissions issued by source presentation/conversion.
97    pub source_submissions: u64,
98    /// Source passes converting a decoded picture into the working space.
99    pub source_conversions: u64,
100    /// Decoded 4:2:0 pictures copied plane for plane into NV12 targets.
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/// Pipelines this renderer created and the time it took.
115#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
116pub struct PipelineCreationStats {
117    /// Pipelines created.
118    pub created: u64,
119    /// Microseconds their creation took in all.
120    pub create_micros_total: u64,
121    /// Microseconds the slowest one took.
122    pub create_micros_max: u64,
123}
124
125/// How frames since the last [`crate::Renderer::take_gpu_submission_stats`] were split into
126/// submissions.
127#[derive(Copy, Clone, Debug, Default, PartialEq)]
128pub struct GpuSubmissionStats {
129    /// Frames rendered.
130    pub frames: u64,
131    /// Frames submitted in more than one submission.
132    pub split_frames: u64,
133    /// The most submissions one frame took.
134    pub max_per_frame: u32,
135    /// Longest predicted GPU time of one submission, in milliseconds, among planned frames.
136    pub max_predicted_ms: Option<f64>,
137}
138
139/// One pipeline creation for tracing.
140#[derive(Clone, Debug, PartialEq)]
141pub struct PipelineCreation {
142    /// An effect's ID or the composite program's label.
143    pub label: String,
144    /// When its creation began.
145    pub started: web_time::Instant,
146    /// When it ended.
147    pub finished: web_time::Instant,
148}
149
150/// Reproducible metadata envelope written beside benchmark measurements.
151#[derive(Clone, Debug, PartialEq)]
152pub struct BenchmarkMetadata {
153    /// Caller-defined project or fixture identity.
154    pub project: String,
155    /// Version of the alpha2 render crate.
156    pub alpha2_version: &'static str,
157    /// Adapter and driver identity.
158    pub adapter: AdapterDiagnostics,
159    /// Output width and height.
160    pub dimensions: (u32, u32),
161    /// Output scale used by the caller.
162    pub resolution_scale: f32,
163    /// Filter sample budget.
164    pub max_filter_samples: u32,
165    /// Temporal sample count.
166    pub motion_blur_subframes: u32,
167    /// Texture byte budget.
168    pub texture_budget_bytes: u64,
169}
170
171/// Entries retained while pass timing is enabled.
172const PASS_HISTORY: usize = 256;
173
174/// Pipeline creations kept until taken; oldest are dropped first.
175const PIPELINE_HISTORY: usize = 256;
176
177#[derive(Debug, Default)]
178pub(crate) struct DiagnosticsState {
179    passes: VecDeque<PassTiming>,
180    /// Incremented when timing turns off, so late GPU timestamp reads are dropped.
181    timing_epoch: u64,
182    presented_frames: u64,
183    dropped_frames: u64,
184    repeated_frames: u64,
185    decoder_wait: Duration,
186    graph_nodes: u64,
187    graph_passes: u64,
188    graph_submissions: u64,
189    source_submissions: u64,
190    source_conversions: u64,
191    yuv_repacks: u64,
192    readback_submissions: u64,
193    frame_submissions: u64,
194    render_passes: u64,
195    copy_operations: u64,
196    copy_bytes: u64,
197    pipeline_creation: PipelineCreationStats,
198    pipeline_creations: VecDeque<PipelineCreation>,
199    gpu_submissions: GpuSubmissionStats,
200}
201
202pub(crate) type SharedDiagnostics = Arc<Mutex<DiagnosticsState>>;
203
204#[derive(Copy, Clone)]
205pub(crate) struct ResourceDiagnostics {
206    pub(crate) textures: TexturePoolStats,
207    pub(crate) upload_textures: UploadTexturePoolStats,
208    pub(crate) readback_buffers: ReadbackBufferPoolStats,
209    pub(crate) uniform_buffers: UniformBufferPoolStats,
210    pub(crate) pipelines: PipelineCacheStats,
211    pub(crate) result_cache: crate::ResultCacheStats,
212}
213
214/// See [`crate::Renderer::pass_timing_epoch`].
215pub(crate) fn timing_epoch(state: &SharedDiagnostics) -> u64 {
216    state
217        .lock()
218        .unwrap_or_else(std::sync::PoisonError::into_inner)
219        .timing_epoch
220}
221
222/// Records one pass duration unless timing was turned off since `epoch`.
223pub(crate) fn record_pass(
224    state: &SharedDiagnostics,
225    epoch: u64,
226    label: &'static str,
227    duration: Duration,
228    source: TimingSource,
229) {
230    let mut state = state
231        .lock()
232        .unwrap_or_else(std::sync::PoisonError::into_inner);
233    if state.timing_epoch != epoch {
234        return;
235    }
236    if state.passes.len() == PASS_HISTORY {
237        state.passes.pop_front();
238    }
239    state.passes.push_back(PassTiming {
240        label,
241        nanoseconds: u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX),
242        source,
243    });
244}
245
246pub(crate) fn record_graph_work(
247    state: &SharedDiagnostics,
248    nodes: u64,
249    render_passes: u64,
250    standalone_submission: bool,
251) {
252    let mut state = state
253        .lock()
254        .unwrap_or_else(std::sync::PoisonError::into_inner);
255    state.graph_nodes = state.graph_nodes.saturating_add(nodes);
256    state.graph_passes = state.graph_passes.saturating_add(render_passes);
257    if standalone_submission {
258        state.graph_submissions = state.graph_submissions.saturating_add(1);
259    }
260    state.render_passes = state.render_passes.saturating_add(render_passes);
261}
262
263pub(crate) fn record_source_work(
264    state: &SharedDiagnostics,
265    render_passes: u64,
266    standalone_submission: bool,
267    conversion: bool,
268) {
269    let mut state = state
270        .lock()
271        .unwrap_or_else(std::sync::PoisonError::into_inner);
272    state.source_conversions = state
273        .source_conversions
274        .saturating_add(u64::from(conversion));
275    if standalone_submission {
276        state.source_submissions = state.source_submissions.saturating_add(1);
277    }
278    state.render_passes = state.render_passes.saturating_add(render_passes);
279}
280
281pub(crate) fn record_readback_work(
282    state: &SharedDiagnostics,
283    render_passes: u64,
284    copy_operations: u64,
285    copy_bytes: u64,
286    standalone_submission: bool,
287) {
288    let mut state = state
289        .lock()
290        .unwrap_or_else(std::sync::PoisonError::into_inner);
291    if standalone_submission {
292        state.readback_submissions = state.readback_submissions.saturating_add(1);
293    }
294    state.render_passes = state.render_passes.saturating_add(render_passes);
295    state.copy_operations = state.copy_operations.saturating_add(copy_operations);
296    state.copy_bytes = state.copy_bytes.saturating_add(copy_bytes);
297}
298
299pub(crate) fn record_frame_submission(state: &SharedDiagnostics) {
300    let mut state = state
301        .lock()
302        .unwrap_or_else(std::sync::PoisonError::into_inner);
303    state.frame_submissions = state.frame_submissions.saturating_add(1);
304}
305
306/// Drops pass history and ends the timing run.
307pub(crate) fn end_timing_run(state: &SharedDiagnostics) {
308    let mut state = state
309        .lock()
310        .unwrap_or_else(std::sync::PoisonError::into_inner);
311    state.passes.clear();
312    state.timing_epoch = state.timing_epoch.wrapping_add(1);
313}
314
315pub(crate) fn snapshot(
316    state: &SharedDiagnostics,
317    capabilities: &Capabilities,
318    resources: &ResourceDiagnostics,
319    max_filter_samples: u32,
320    motion_blur_subframes: u32,
321    degradations: Vec<crate::CapabilityDiagnostic>,
322) -> RenderDiagnostics {
323    let state = state
324        .lock()
325        .unwrap_or_else(std::sync::PoisonError::into_inner);
326    RenderDiagnostics {
327        adapter: AdapterDiagnostics {
328            name: capabilities.adapter_name.clone(),
329            backend: capabilities.backend.clone(),
330            driver: capabilities.driver.clone(),
331            driver_info: capabilities.driver_info.clone(),
332            tier: capabilities.tier,
333            timestamp_queries: capabilities.timestamp_queries,
334            shader_compiler: capabilities.shader_compiler.clone(),
335        },
336        passes: state.passes.iter().copied().collect(),
337        textures: resources.textures,
338        upload_textures: resources.upload_textures,
339        readback_buffers: resources.readback_buffers,
340        uniform_buffers: resources.uniform_buffers,
341        pipelines: resources.pipelines,
342        pipeline_creation: state.pipeline_creation,
343        result_cache: resources.result_cache,
344        presented_frames: state.presented_frames,
345        dropped_frames: state.dropped_frames,
346        repeated_frames: state.repeated_frames,
347        decoder_wait: state.decoder_wait,
348        max_filter_samples,
349        motion_blur_subframes,
350        degradations,
351        graph_nodes: state.graph_nodes,
352        graph_passes: state.graph_passes,
353        graph_submissions: state.graph_submissions,
354        source_submissions: state.source_submissions,
355        source_conversions: state.source_conversions,
356        yuv_repacks: state.yuv_repacks,
357        readback_submissions: state.readback_submissions,
358        frame_submissions: state.frame_submissions,
359        render_passes: state.render_passes,
360        copy_operations: state.copy_operations,
361        copy_bytes: state.copy_bytes,
362    }
363}
364
365/// Runs `create`, recording its duration under `label`.
366pub(crate) fn timed_pipeline_creation<T>(
367    state: &SharedDiagnostics,
368    label: impl FnOnce() -> String,
369    create: impl FnOnce() -> T,
370) -> T {
371    let started = web_time::Instant::now();
372    let created = create();
373    let finished = web_time::Instant::now();
374    let micros = u64::try_from(finished.duration_since(started).as_micros()).unwrap_or(u64::MAX);
375    let mut state = state
376        .lock()
377        .unwrap_or_else(std::sync::PoisonError::into_inner);
378    let totals = &mut state.pipeline_creation;
379    totals.created = totals.created.saturating_add(1);
380    totals.create_micros_total = totals.create_micros_total.saturating_add(micros);
381    totals.create_micros_max = totals.create_micros_max.max(micros);
382    if state.pipeline_creations.len() == PIPELINE_HISTORY {
383        state.pipeline_creations.pop_front();
384    }
385    state.pipeline_creations.push_back(PipelineCreation {
386        label: label(),
387        started,
388        finished,
389    });
390    created
391}
392
393pub(crate) fn take_pipeline_creations(state: &SharedDiagnostics) -> Vec<PipelineCreation> {
394    state
395        .lock()
396        .unwrap_or_else(std::sync::PoisonError::into_inner)
397        .pipeline_creations
398        .drain(..)
399        .collect()
400}
401
402/// Records how one frame was submitted and its longest predicted submission.
403pub(crate) fn record_frame_plan(
404    state: &SharedDiagnostics,
405    frame: &crate::FrameSubmissions,
406    max_predicted_ms: Option<f64>,
407) {
408    let mut state = state
409        .lock()
410        .unwrap_or_else(std::sync::PoisonError::into_inner);
411    let totals = &mut state.gpu_submissions;
412    totals.frames = totals.frames.saturating_add(1);
413    totals.split_frames = totals
414        .split_frames
415        .saturating_add(u64::from(frame.submissions > 1));
416    totals.max_per_frame = totals.max_per_frame.max(frame.submissions);
417    if let Some(ms) = max_predicted_ms {
418        totals.max_predicted_ms = Some(totals.max_predicted_ms.map_or(ms, |most| most.max(ms)));
419    }
420}
421
422/// Submission counts since the last call, then resets them.
423pub(crate) fn take_gpu_submission_stats(state: &SharedDiagnostics) -> GpuSubmissionStats {
424    std::mem::take(
425        &mut state
426            .lock()
427            .unwrap_or_else(std::sync::PoisonError::into_inner)
428            .gpu_submissions,
429    )
430}
431
432pub(crate) fn record_yuv_repack(state: &SharedDiagnostics) {
433    let mut state = state
434        .lock()
435        .unwrap_or_else(std::sync::PoisonError::into_inner);
436    state.yuv_repacks = state.yuv_repacks.saturating_add(1);
437}
438
439pub(crate) fn record_frame(state: &SharedDiagnostics, presented: bool, repeated: bool) {
440    let mut state = state
441        .lock()
442        .unwrap_or_else(std::sync::PoisonError::into_inner);
443    state.presented_frames += u64::from(presented);
444    state.dropped_frames += u64::from(!presented);
445    state.repeated_frames += u64::from(repeated);
446}
447
448pub(crate) fn record_decoder_wait(state: &SharedDiagnostics, duration: Duration) {
449    let mut state = state
450        .lock()
451        .unwrap_or_else(std::sync::PoisonError::into_inner);
452    state.decoder_wait = state.decoder_wait.saturating_add(duration);
453}