use std::sync::{Arc, Mutex};
pub struct GpuTimer {
inner: Option<Timer>,
}
struct Timer {
query_set: wgpu::QuerySet,
resolve: wgpu::Buffer,
staging: Vec<Slot>,
period: f32,
frame: usize,
last_ms: f32,
}
struct Slot {
buffer: wgpu::Buffer,
state: Arc<Mutex<SlotState>>,
}
#[derive(PartialEq)]
enum SlotState {
Free,
Copied,
Pending,
Ready,
}
const RING: usize = 3;
impl GpuTimer {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
if !device.features().contains(wgpu::Features::TIMESTAMP_QUERY) {
return Self { inner: None };
}
let query_set = device.create_query_set(&wgpu::QuerySetDescriptor {
label: Some("valo.gpu_timer"),
ty: wgpu::QueryType::Timestamp,
count: 2,
});
let resolve = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("valo.gpu_timer.resolve"),
size: 16,
usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging = (0..RING)
.map(|_| Slot {
buffer: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("valo.gpu_timer.staging"),
size: 16,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
state: Arc::new(Mutex::new(SlotState::Free)),
})
.collect();
Self {
inner: Some(Timer {
query_set,
resolve,
staging,
period: queue.get_timestamp_period(),
frame: 0,
last_ms: 0.0,
}),
}
}
pub fn pass_writes(
&self,
index: usize,
count: usize,
) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
let timer = self.inner.as_ref()?;
let first = index == 0;
let last = index + 1 == count;
if !first && !last {
return None;
}
Some(wgpu::RenderPassTimestampWrites {
query_set: &timer.query_set,
beginning_of_pass_write_index: first.then_some(0),
end_of_pass_write_index: last.then_some(1),
})
}
pub fn end_frame(&mut self, encoder: &mut wgpu::CommandEncoder) {
let Some(timer) = &mut self.inner else {
return;
};
encoder.resolve_query_set(&timer.query_set, 0..2, &timer.resolve, 0);
timer.frame += 1;
let slot = &timer.staging[timer.frame % RING];
let mut state = slot.state.lock().expect("timer lock");
if *state != SlotState::Free {
return;
}
*state = SlotState::Copied;
encoder.copy_buffer_to_buffer(&timer.resolve, 0, &slot.buffer, 0, 16);
}
pub fn after_submit(&self) {
let Some(timer) = &self.inner else {
return;
};
let slot = &timer.staging[timer.frame % RING];
{
let mut state = slot.state.lock().expect("timer lock");
if *state != SlotState::Copied {
return;
}
*state = SlotState::Pending;
}
let state = slot.state.clone();
slot.buffer
.slice(..)
.map_async(wgpu::MapMode::Read, move |result| {
let mut state = state.lock().expect("timer lock");
*state = if result.is_ok() {
SlotState::Ready
} else {
SlotState::Free
};
});
}
pub fn latest_ms(&mut self, device: &wgpu::Device) -> f32 {
let Some(timer) = &mut self.inner else {
return 0.0;
};
let _ = device.poll(wgpu::PollType::Poll);
for slot in &timer.staging {
if *slot.state.lock().expect("timer lock") != SlotState::Ready {
continue;
}
let ticks: Vec<u64> = {
let view = slot
.buffer
.get_mapped_range(..)
.expect("timer staging maps");
bytemuck::cast_slice(&view).to_vec()
};
slot.buffer.unmap();
*slot.state.lock().expect("timer lock") = SlotState::Free;
timer.last_ms = ticks[1].saturating_sub(ticks[0]) as f32 * timer.period / 1e6;
}
timer.last_ms
}
}