wgpu_core/timestamp_normalization/
mod.rs1use core::num::NonZeroU64;
31
32use alloc::{boxed::Box, string::String, sync::Arc};
33
34use hashbrown::HashMap;
35
36use crate::{
37 device::{Device, DeviceError},
38 hal_label,
39 pipeline::{CreatePipelineError, CreateShaderModuleError},
40 resource::Buffer,
41 snatch::SnatchGuard,
42 track::BufferTracker,
43};
44
45pub const TIMESTAMP_NORMALIZATION_BUFFER_USES: wgt::BufferUses =
46 wgt::BufferUses::STORAGE_READ_WRITE;
47
48struct InternalState {
49 temporary_bind_group_layout: Box<dyn hal::DynBindGroupLayout>,
50 pipeline_layout: Box<dyn hal::DynPipelineLayout>,
51 pipeline: Box<dyn hal::DynComputePipeline>,
52}
53
54#[derive(Debug, Clone, thiserror::Error)]
55pub enum TimestampNormalizerInitError {
56 #[error("Failed to initialize bind group layout")]
57 BindGroupLayout(#[source] DeviceError),
58 #[cfg(feature = "wgsl")]
59 #[error("Failed to parse shader")]
60 ParseWgsl(#[source] naga::error::ShaderError<naga::front::wgsl::ParseError>),
61 #[error("Failed to validate shader module")]
62 ValidateWgsl(#[source] naga::error::ShaderError<naga::WithSpan<naga::valid::ValidationError>>),
63 #[error("Failed to create shader module")]
64 CreateShaderModule(#[from] CreateShaderModuleError),
65 #[error("Failed to create pipeline layout")]
66 PipelineLayout(#[source] DeviceError),
67 #[error("Failed to create compute pipeline")]
68 ComputePipeline(#[from] CreatePipelineError),
69}
70
71pub struct TimestampNormalizer {
74 state: Option<InternalState>,
75}
76
77impl TimestampNormalizer {
78 pub fn new(
87 device: &Device,
88 timestamp_period: f32,
89 ) -> Result<Self, TimestampNormalizerInitError> {
90 unsafe {
91 if !device
92 .instance_flags
93 .contains(wgt::InstanceFlags::AUTOMATIC_TIMESTAMP_NORMALIZATION)
94 {
95 return Ok(Self { state: None });
96 }
97
98 if !device
99 .downlevel
100 .flags
101 .contains(wgt::DownlevelFlags::COMPUTE_SHADERS)
102 {
103 log::error!("Automatic timestamp normalization was requested, but compute shaders are not supported.");
104 return Ok(Self { state: None });
105 }
106
107 if timestamp_period == 1.0 {
108 return Ok(Self { state: None });
110 }
111
112 let temporary_bind_group_layout = device
113 .raw()
114 .create_bind_group_layout(&hal::BindGroupLayoutDescriptor {
115 label: hal_label(
116 Some("(wgpu internal) Timestamp Normalization Bind Group Layout"),
117 device.instance_flags,
118 ),
119 flags: hal::BindGroupLayoutFlags::empty(),
120 entries: &[wgt::BindGroupLayoutEntry {
121 binding: 0,
122 visibility: wgt::ShaderStages::COMPUTE,
123 ty: wgt::BindingType::Buffer {
124 ty: wgt::BufferBindingType::Storage { read_only: false },
125 has_dynamic_offset: false,
126 min_binding_size: Some(NonZeroU64::new(8).unwrap()),
127 },
128 count: None,
129 }],
130 })
131 .map_err(|e| {
132 TimestampNormalizerInitError::BindGroupLayout(device.handle_hal_error(e))
133 })?;
134
135 let common_src = include_str!("common.wgsl");
136 let src = include_str!("timestamp_normalization.wgsl");
137
138 let preprocessed_src = alloc::format!("{common_src}\n{src}");
139
140 #[cfg(feature = "wgsl")]
141 let module = naga::front::wgsl::parse_str(&preprocessed_src).map_err(|inner| {
142 TimestampNormalizerInitError::ParseWgsl(naga::error::ShaderError {
143 source: preprocessed_src.clone(),
144 label: None,
145 inner: Box::new(inner),
146 })
147 })?;
148 #[cfg(not(feature = "wgsl"))]
149 #[allow(clippy::diverging_sub_expression)]
150 let module =
151 panic!("Timestamp normalization requires the wgsl feature flag to be enabled!");
152
153 let info = crate::device::create_validator(
154 wgt::Features::IMMEDIATES,
155 wgt::DownlevelFlags::empty(),
156 naga::valid::ValidationFlags::all(),
157 )
158 .validate(&module)
159 .map_err(|inner| {
160 TimestampNormalizerInitError::ValidateWgsl(naga::error::ShaderError {
161 source: preprocessed_src.clone(),
162 label: None,
163 inner,
164 })
165 })?;
166 let hal_shader = hal::ShaderInput::Naga(hal::NagaShader {
167 module: alloc::borrow::Cow::Owned(module),
168 info,
169 debug_source: None,
170 });
171 let hal_desc = hal::ShaderModuleDescriptor {
172 label: hal_label(
173 Some("(wgpu internal) Timestamp normalizer shader module"),
174 device.instance_flags,
175 ),
176 runtime_checks: wgt::ShaderRuntimeChecks::unchecked(),
177 };
178 let module = device
179 .raw()
180 .create_shader_module(&hal_desc, hal_shader)
181 .map_err(|error| match error {
182 hal::ShaderError::Device(error) => {
183 CreateShaderModuleError::Device(device.handle_hal_error(error))
184 }
185 hal::ShaderError::Compilation(ref msg) => {
186 log::error!("Shader error: {msg}");
187 CreateShaderModuleError::Generation
188 }
189 })?;
190
191 let pipeline_layout = device
192 .raw()
193 .create_pipeline_layout(&hal::PipelineLayoutDescriptor {
194 label: hal_label(
195 Some("(wgpu internal) Timestamp normalizer pipeline layout"),
196 device.instance_flags,
197 ),
198 bind_group_layouts: &[Some(temporary_bind_group_layout.as_ref())],
199 immediate_size: 8,
200 flags: hal::PipelineLayoutFlags::empty(),
201 })
202 .map_err(|e| {
203 TimestampNormalizerInitError::PipelineLayout(device.handle_hal_error(e))
204 })?;
205
206 let (multiplier, shift) = compute_timestamp_period(timestamp_period);
207
208 let mut constants = HashMap::with_capacity(2);
209 constants.insert(String::from("TIMESTAMP_PERIOD_MULTIPLY"), multiplier as f64);
210 constants.insert(String::from("TIMESTAMP_PERIOD_SHIFT"), shift as f64);
211
212 let pipeline_desc = hal::ComputePipelineDescriptor {
213 label: hal_label(
214 Some("(wgpu internal) Timestamp normalizer pipeline"),
215 device.instance_flags,
216 ),
217 layout: pipeline_layout.as_ref(),
218 stage: hal::ProgrammableStage {
219 module: module.as_ref(),
220 entry_point: "main",
221 constants: &constants,
222 zero_initialize_workgroup_memory: false,
223 },
224 cache: None,
225 };
226 let pipeline = device
227 .raw()
228 .create_compute_pipeline(&pipeline_desc)
229 .map_err(|error| device.handle_hal_pipeline_error(error))?;
230
231 Ok(Self {
232 state: Some(InternalState {
233 temporary_bind_group_layout,
234 pipeline_layout,
235 pipeline,
236 }),
237 })
238 }
239 }
240
241 pub unsafe fn create_normalization_bind_group(
254 &self,
255 device: &Device,
256 buffer: &dyn hal::DynBuffer,
257 buffer_label: Option<&str>,
258 buffer_size: wgt::BufferSize,
259 buffer_usages: wgt::BufferUsages,
260 ) -> Result<TimestampNormalizationBindGroup, DeviceError> {
261 unsafe {
262 let Some(ref state) = &self.state else {
263 return Ok(TimestampNormalizationBindGroup { raw: None });
264 };
265
266 if !buffer_usages.contains(wgt::BufferUsages::QUERY_RESOLVE) {
267 return Ok(TimestampNormalizationBindGroup { raw: None });
268 }
269
270 if buffer_size.get() > device.adapter.limits().max_storage_buffer_binding_size {
275 return Err(DeviceError::OutOfMemory);
276 }
277
278 let bg_label_alloc;
279 let label = match buffer_label {
280 Some(label) => {
281 bg_label_alloc = alloc::format!("Timestamp normalization bind group ({label})");
282 &*bg_label_alloc
283 }
284 None => "Timestamp normalization bind group",
285 };
286
287 let bg = device
288 .raw()
289 .create_bind_group(&hal::BindGroupDescriptor {
290 label: hal_label(Some(label), device.instance_flags),
291 layout: &*state.temporary_bind_group_layout,
292 buffers: &[hal::BufferBinding::new_unchecked(buffer, 0, buffer_size)],
293 samplers: &[],
294 textures: &[],
295 acceleration_structures: &[],
296 external_textures: &[],
297 entries: &[hal::BindGroupEntry {
298 binding: 0,
299 resource_index: 0,
300 count: 1,
301 }],
302 })
303 .map_err(|e| device.handle_hal_error(e))?;
304
305 Ok(TimestampNormalizationBindGroup { raw: Some(bg) })
306 }
307 }
308
309 pub fn normalize(
310 &self,
311 snatch_guard: &SnatchGuard<'_>,
312 encoder: &mut dyn hal::DynCommandEncoder,
313 tracker: &mut BufferTracker,
314 bind_group: &TimestampNormalizationBindGroup,
315 buffer: &Arc<Buffer>,
316 buffer_offset_bytes: u64,
317 total_timestamps: u32,
318 ) {
319 let Some(ref state) = &self.state else {
320 return;
321 };
322
323 let Some(bind_group) = bind_group.raw.as_deref() else {
324 return;
325 };
326
327 let buffer_offset_timestamps: u32 = (buffer_offset_bytes / 8).try_into().unwrap(); let pending_barrier = tracker.set_single(buffer, wgt::BufferUses::STORAGE_READ_WRITE);
330
331 let barrier = pending_barrier.map(|pending| pending.into_hal(buffer, snatch_guard));
332
333 let needed_workgroups = total_timestamps.div_ceil(64);
334
335 unsafe {
336 encoder.transition_buffers(barrier.as_slice());
337 encoder.begin_compute_pass(&hal::ComputePassDescriptor {
338 label: hal_label(
339 Some("(wgpu internal) Timestamp normalization pass"),
340 buffer.device.instance_flags,
341 ),
342 timestamp_writes: None,
343 });
344 encoder.set_compute_pipeline(&*state.pipeline);
345 encoder.set_bind_group(&*state.pipeline_layout, 0, bind_group, &[]);
346 encoder.set_immediates(
347 &*state.pipeline_layout,
348 0,
349 &[buffer_offset_timestamps, total_timestamps],
350 );
351 encoder.dispatch_workgroups([needed_workgroups, 1, 1]);
352 encoder.end_compute_pass();
353 }
354 }
355
356 pub fn dispose(self, device: &dyn hal::DynDevice) {
357 unsafe {
358 let Some(state) = self.state else {
359 return;
360 };
361
362 device.destroy_compute_pipeline(state.pipeline);
363 device.destroy_pipeline_layout(state.pipeline_layout);
364 device.destroy_bind_group_layout(state.temporary_bind_group_layout);
365 }
366 }
367
368 pub fn enabled(&self) -> bool {
369 self.state.is_some()
370 }
371}
372
373#[derive(Debug)]
374pub struct TimestampNormalizationBindGroup {
375 raw: Option<Box<dyn hal::DynBindGroup>>,
376}
377
378impl TimestampNormalizationBindGroup {
379 pub fn dispose(self, device: &dyn hal::DynDevice) {
380 unsafe {
381 if let Some(raw) = self.raw {
382 device.destroy_bind_group(raw);
383 }
384 }
385 }
386}
387
388fn compute_timestamp_period(input: f32) -> (u32, u32) {
389 #[cfg(not(feature = "std"))]
390 use num_traits::float::Float as _;
391
392 let pow2 = input.log2().ceil() as i32;
393 let clamped_pow2 = pow2.clamp(-32, 32).unsigned_abs();
394 let shift = 32 - clamped_pow2;
395
396 let denominator = (1u64 << shift) as f64;
397
398 let multiplier = (input as f64 * denominator).round() as u32;
400
401 (multiplier, shift)
402}
403
404#[cfg(test)]
405mod tests {
406 use core::f64;
407
408 fn assert_timestamp_case(input: f32) {
409 let (multiplier, shift) = super::compute_timestamp_period(input);
410
411 let output = multiplier as f64 / (1u64 << shift) as f64;
412
413 assert!((input as f64 - output).abs() < 0.0000001);
414 }
415
416 #[test]
417 fn compute_timestamp_period() {
418 assert_timestamp_case(0.01);
419 assert_timestamp_case(0.5);
420 assert_timestamp_case(1.0);
421 assert_timestamp_case(2.0);
422 assert_timestamp_case(2.7);
423 assert_timestamp_case(1000.7);
424 }
425}