use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
const RING: usize = 3;
const FRAME_BYTES: u64 = 16;
struct Slot {
resolve: wgpu::Buffer,
readback: wgpu::Buffer,
ready: Arc<AtomicBool>,
pending: bool,
}
pub(crate) struct GpuTimer {
queries: wgpu::QuerySet,
slots: Vec<Slot>,
next: usize,
period: f32,
last_ms: Option<f32>,
}
impl GpuTimer {
pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Option<Self> {
if !device.features().contains(wgpu::Features::TIMESTAMP_QUERY) {
return None;
}
let queries = device.create_query_set(&wgpu::QuerySetDescriptor {
label: Some("frus.gpu_timer.queries"),
ty: wgpu::QueryType::Timestamp,
count: (RING * 2) as u32,
});
let slots = (0..RING)
.map(|_| Slot {
resolve: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frus.gpu_timer.resolve"),
size: FRAME_BYTES,
usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
}),
readback: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frus.gpu_timer.readback"),
size: FRAME_BYTES,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
ready: Arc::new(AtomicBool::new(false)),
pending: false,
})
.collect();
Some(Self {
queries,
slots,
next: 0,
period: queue.get_timestamp_period(),
last_ms: None,
})
}
pub(crate) fn last_ms(&self) -> Option<f32> {
self.last_ms
}
fn collect(&mut self, device: &wgpu::Device) {
let _ = device.poll(wgpu::PollType::Poll);
for slot in &mut self.slots {
if !(slot.pending && slot.ready.load(Ordering::Acquire)) {
continue;
}
if let Ok(view) = slot.readback.get_mapped_range(..) {
let start = u64::from_le_bytes(view[0..8].try_into().unwrap_or([0; 8]));
let end = u64::from_le_bytes(view[8..16].try_into().unwrap_or([0; 8]));
drop(view);
if end > start {
self.last_ms = Some((end - start) as f32 * self.period / 1_000_000.0);
}
}
slot.readback.unmap();
slot.ready.store(false, Ordering::Release);
slot.pending = false;
}
}
pub(crate) fn begin(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) -> Option<usize> {
self.collect(device);
let at = self.next;
if self.slots[at].pending {
return None;
}
self.next = (self.next + 1) % RING;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frus.gpu_timer.begin"),
});
mark(&mut encoder, &self.queries, (at * 2) as u32);
queue.submit(std::iter::once(encoder.finish()));
Some(at)
}
pub(crate) fn end(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, at: usize) {
let slot = &mut self.slots[at];
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frus.gpu_timer.end"),
});
mark(&mut encoder, &self.queries, (at * 2 + 1) as u32);
let first = (at * 2) as u32;
encoder.resolve_query_set(&self.queries, first..first + 2, &slot.resolve, 0);
encoder.copy_buffer_to_buffer(&slot.resolve, 0, &slot.readback, 0, FRAME_BYTES);
queue.submit(std::iter::once(encoder.finish()));
let ready = slot.ready.clone();
slot.readback
.map_async(wgpu::MapMode::Read, .., move |result| {
if result.is_ok() {
ready.store(true, Ordering::Release);
}
});
slot.pending = true;
}
}
fn mark(encoder: &mut wgpu::CommandEncoder, queries: &wgpu::QuerySet, index: u32) {
let _pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("frus.gpu_timer.mark"),
timestamp_writes: Some(wgpu::ComputePassTimestampWrites {
query_set: queries,
beginning_of_pass_write_index: Some(index),
end_of_pass_write_index: None,
}),
});
}
#[cfg(test)]
mod tests {
use super::*;
fn timed_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: None,
force_fallback_adapter: false,
..Default::default()
}))
.ok()?;
if !adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY) {
return None;
}
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("frus.gpu_timer.test.device"),
required_features: wgpu::Features::TIMESTAMP_QUERY,
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::default(),
..Default::default()
}))
.ok()
}
#[test]
fn a_device_without_a_clock_has_no_timer() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
eprintln!("no GPU adapter available: test skipped");
return;
};
let Ok((device, queue)) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))
else {
return;
};
assert!(GpuTimer::new(&device, &queue).is_none());
}
#[test]
fn frames_timed_by_the_gpu_come_back() {
let Some((device, queue)) = timed_device() else {
eprintln!("no GPU clock on this machine: test skipped");
return;
};
let mut timer = GpuTimer::new(&device, &queue).expect("a timer");
assert_eq!(timer.last_ms(), None, "nothing has been timed yet");
for _ in 0..20 {
if let Some(at) = timer.begin(&device, &queue) {
timer.end(&device, &queue, at);
}
let _ = device.poll(wgpu::PollType::wait_indefinitely());
}
let _ = timer.begin(&device, &queue);
let ms = timer.last_ms().expect("a frame came back");
assert!((0.0..1000.0).contains(&ms), "{ms} ms");
}
}