Skip to main content

molgfx_gpu/
timestamp_capture.rs

1//! Bounded timestamp metadata collected at actual command-encoder pass boundaries.
2//!
3//! Storage and queries are allocated once. Move the capture into an encoder,
4//! take it back before submission to resolve its written range, then retain it
5//! until GPU completion and decoding before resetting for another submission.
6
7use crate::{Device, GpuError};
8use std::ops::Range;
9
10/// Portable ceiling: each pass consumes two of the 4096 query slots.
11pub const MAX_TIMESTAMP_CAPTURE_PASSES: u32 = 2048;
12
13/// Failure to allocate an opt-in capture.
14#[derive(Debug, thiserror::Error)]
15pub enum TimestampCaptureError {
16    /// The opened device does not support pass-boundary timestamp queries.
17    #[error("pass timestamp queries are unsupported")]
18    Unsupported,
19    /// Capacity must be nonzero and fit the portable query-set ceiling.
20    #[error("invalid timestamp capture capacity {requested}")]
21    InvalidCapacity {
22        /// Requested number of pass occurrences.
23        requested: u32,
24    },
25    /// Query allocation failed.
26    #[error(transparent)]
27    Backend(#[from] GpuError),
28}
29
30/// The command kind at the actual recording boundary.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum TimestampPassKind {
33    /// A render pass.
34    Render,
35    /// A compute pass.
36    Compute,
37}
38
39/// Two unique indices for one captured occurrence.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub struct TimestampQueryPair {
42    /// Beginning-of-pass query index.
43    pub beginning: u32,
44    /// End-of-pass query index.
45    pub end: u32,
46}
47
48/// Why one recorded pass has no capture-owned timestamp pair.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum PassTimestampAbsence {
51    /// Caller-authored descriptor writes take precedence and remain unchanged.
52    DescriptorTimestampWrites,
53    /// Metadata-only capture has no supported timestamp query resource.
54    Unsupported,
55}
56
57/// A pass occurrence, not a render-graph node or a label aggregate.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub struct PassTimestampRecord {
60    /// Recording order, including occurrences omitted after capacity is exhausted.
61    pub occurrence: u64,
62    /// Descriptor label, borrowed without a string allocation.
63    pub label: &'static str,
64    /// Actual pass kind.
65    pub kind: TimestampPassKind,
66    /// Optional caller-provided temporal or exposure sample identity.
67    pub sample: Option<u32>,
68    /// Capture-owned pair or a reason that this descriptor could not be captured.
69    pub queries: Result<TimestampQueryPair, PassTimestampAbsence>,
70}
71
72/// Why the bounded capture cannot represent every pass occurrence.
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum TimestampCaptureIncomplete {
75    /// Rendering continued unchanged, but the metadata/query budget was exhausted.
76    CapacityExceeded {
77        /// Configured maximum number of metadata records.
78        capacity: u32,
79        /// Number of omitted pass occurrences.
80        omitted_passes: u64,
81    },
82}
83
84/// Reusable storage and query ownership for one submission's actual passes.
85#[derive(Debug)]
86pub struct PassTimestampCapture<Q> {
87    queries: Option<Q>,
88    records: Vec<PassTimestampRecord>,
89    max_passes: u32,
90    next_query: u32,
91    passes_seen: u64,
92    sample: Option<u32>,
93}
94
95impl<Q> PassTimestampCapture<Q> {
96    /// Allocates the query set and all metadata storage outside command recording.
97    ///
98    /// # Errors
99    /// Returns typed unsupported, capacity, or backend allocation failures.
100    pub fn new<D: Device<QuerySet = Q>>(
101        device: &D,
102        max_passes: u32,
103    ) -> Result<Self, TimestampCaptureError> {
104        if max_passes == 0 || max_passes > MAX_TIMESTAMP_CAPTURE_PASSES {
105            return Err(TimestampCaptureError::InvalidCapacity {
106                requested: max_passes,
107            });
108        }
109        if !device.capabilities().timestamp_queries() {
110            return Err(TimestampCaptureError::Unsupported);
111        }
112        let queries = device.create_timestamp_query_set(max_passes * 2)?;
113        Ok(Self {
114            queries: Some(queries),
115            records: Vec::with_capacity(max_passes as usize),
116            max_passes,
117            next_query: 0,
118            passes_seen: 0,
119            sample: None,
120        })
121    }
122
123    /// Allocates only bounded metadata for a device without timestamp queries.
124    /// No query writes or synthetic timing values are produced.
125    ///
126    /// # Errors
127    /// Returns a capacity error when the requested bound is invalid.
128    pub fn metadata_only(max_passes: u32) -> Result<Self, TimestampCaptureError> {
129        if max_passes == 0 || max_passes > MAX_TIMESTAMP_CAPTURE_PASSES {
130            return Err(TimestampCaptureError::InvalidCapacity {
131                requested: max_passes,
132            });
133        }
134        Ok(Self {
135            queries: None,
136            records: Vec::with_capacity(max_passes as usize),
137            max_passes,
138            next_query: 0,
139            passes_seen: 0,
140            sample: None,
141        })
142    }
143
144    /// Optional query resource; absent for metadata-only capture.
145    #[must_use]
146    pub const fn queries(&self) -> Option<&Q> {
147        self.queries.as_ref()
148    }
149
150    /// Contiguous written query slots. Skip resolve when this range is empty.
151    #[must_use]
152    pub const fn query_range(&self) -> Range<u32> {
153        0..self.next_query
154    }
155
156    /// Number of allocated query slots, including unused bounded capacity.
157    #[must_use]
158    pub const fn query_capacity(&self) -> u32 {
159        if self.queries.is_some() {
160            self.max_passes * 2
161        } else {
162            0
163        }
164    }
165
166    /// Records retained in command recording order.
167    #[must_use]
168    pub fn records(&self) -> &[PassTimestampRecord] {
169        &self.records
170    }
171
172    /// Total actual passes, including every omitted occurrence after overflow.
173    #[must_use]
174    pub const fn passes_seen(&self) -> u64 {
175        self.passes_seen
176    }
177
178    /// Explicitly reports overflow; retained records must not imply completeness.
179    #[must_use]
180    pub fn incomplete(&self) -> Option<TimestampCaptureIncomplete> {
181        let omitted_passes = self.passes_seen - self.records.len() as u64;
182        (omitted_passes != 0).then_some(TimestampCaptureIncomplete::CapacityExceeded {
183            capacity: self.max_passes,
184            omitted_passes,
185        })
186    }
187
188    /// Sets identity for subsequent pass occurrences; does not alter prior records.
189    pub const fn set_sample(&mut self, sample: Option<u32>) {
190        self.sample = sample;
191    }
192
193    /// Clears metadata without reallocating. Call only after the preceding GPU
194    /// submission has completed and its queries have been decoded.
195    pub fn reset(&mut self) {
196        self.records.clear();
197        self.next_query = 0;
198        self.passes_seen = 0;
199        self.sample = None;
200    }
201
202    /// Backend hook at pass creation. Descriptor-authored timestamps are never
203    /// replaced, and no query is reused if the bounded capacity is exhausted.
204    pub fn record_pass(
205        &mut self,
206        label: &'static str,
207        kind: TimestampPassKind,
208        descriptor_has_timestamps: bool,
209    ) -> Option<TimestampQueryPair> {
210        let occurrence = self.passes_seen;
211        self.passes_seen = self.passes_seen.saturating_add(1);
212        if self.records.len() == self.max_passes as usize {
213            return None;
214        }
215        let queries = if descriptor_has_timestamps {
216            Err(PassTimestampAbsence::DescriptorTimestampWrites)
217        } else if self.queries.is_none() {
218            Err(PassTimestampAbsence::Unsupported)
219        } else {
220            let pair = TimestampQueryPair {
221                beginning: self.next_query,
222                end: self.next_query + 1,
223            };
224            self.next_query += 2;
225            Ok(pair)
226        };
227        self.records.push(PassTimestampRecord {
228            occurrence,
229            label,
230            kind,
231            sample: self.sample,
232            queries,
233        });
234        queries.ok()
235    }
236}
237
238#[cfg(test)]
239#[path = "timestamp_capture_tests.rs"]
240mod tests;