use std::sync::{Arc, Mutex, PoisonError};
use crate::profiler::{GpuPass, PassSpan};
use std::sync::atomic::{AtomicU8, Ordering};
const SLOTS: usize = 3;
const MAX_PASSES: u32 = 32;
const QUERIES: u32 = MAX_PASSES * 2;
const BYTES: u64 = QUERIES as u64 * 8;
const FREE: u8 = 0;
const MAPPING: u8 = 1;
const READY: u8 = 2;
struct Slot {
buffer: wgpu::Buffer,
state: Arc<AtomicU8>,
written: u32,
names: Vec<&'static str>,
}
pub(crate) struct GpuTimer {
queries: wgpu::QuerySet,
resolve: wgpu::Buffer,
slots: [Slot; SLOTS],
period: f64,
current: Option<usize>,
next: usize,
names: Mutex<Vec<&'static str>>,
pub smoothed_ms: Option<f32>,
pub passes: Vec<GpuPass>,
pub last_frame: Vec<PassSpan>,
pub frames_read: u64,
scratch: Vec<GpuPass>,
last_end: u64,
order: Vec<(u64, u64, &'static str)>,
}
impl GpuTimer {
pub 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("frame timestamps"),
ty: wgpu::QueryType::Timestamp,
count: QUERIES,
});
let resolve = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("timestamp resolve"),
size: BYTES,
usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let slot = || Slot {
buffer: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("timestamp readback"),
size: BYTES,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
state: Arc::new(AtomicU8::new(FREE)),
written: 0,
names: Vec::with_capacity(MAX_PASSES as usize),
};
Some(Self {
queries,
resolve,
slots: [slot(), slot(), slot()],
period: f64::from(queue.get_timestamp_period()),
current: None,
next: 0,
names: Mutex::new(Vec::with_capacity(MAX_PASSES as usize)),
smoothed_ms: None,
passes: Vec::new(),
last_frame: Vec::with_capacity(MAX_PASSES as usize),
frames_read: 0,
scratch: Vec::with_capacity(MAX_PASSES as usize),
last_end: 0,
order: Vec::with_capacity(MAX_PASSES as usize),
})
}
pub fn collect(&mut self, device: &wgpu::Device) {
let _ = device.poll(wgpu::PollType::Poll);
for k in 0..SLOTS {
let i = (self.next + k) % SLOTS;
if self.slots[i].state.load(Ordering::Acquire) != READY {
continue;
}
let ok = match self.slots[i].buffer.get_mapped_range(..) {
Ok(view) => {
let t: &[u64] = bytemuck::cast_slice(&view);
let slot = &self.slots[i];
let not_before = self.last_end;
read_frame(
&t[..slot.written as usize],
&slot.names,
self.period,
not_before,
&mut self.last_frame,
&mut self.order,
)
}
Err(_) => None,
};
let slot = &mut self.slots[i];
slot.buffer.unmap();
slot.state.store(FREE, Ordering::Release);
let Some((ms, end)) = ok else { continue };
self.last_end = end;
if !(ms.is_finite() && ms < 10_000.0) {
continue;
}
self.frames_read += 1;
self.smoothed_ms = Some(match self.smoothed_ms {
None => ms,
Some(avg) => avg + (ms - avg) * 0.1,
});
smooth_passes(&mut self.passes, &mut self.scratch, &self.last_frame);
}
}
pub fn begin_frame(&mut self) {
let i = self.next;
self.names.get_mut().unwrap_or_else(PoisonError::into_inner).clear();
self.current = (self.slots[i].state.load(Ordering::Acquire) == FREE).then_some(i);
if self.current.is_some() {
self.next = (i + 1) % SLOTS;
}
}
pub fn pass(&self, name: &'static str) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
self.current?;
let at = {
let mut names = self.names.lock().unwrap_or_else(PoisonError::into_inner);
let at = names.len() as u32 * 2;
if at + 2 > QUERIES {
return None;
}
names.push(name);
at
};
Some(wgpu::RenderPassTimestampWrites {
query_set: &self.queries,
beginning_of_pass_write_index: Some(at),
end_of_pass_write_index: Some(at + 1),
})
}
pub fn resolve(&mut self, encoder: &mut wgpu::CommandEncoder) {
let names = self.names.get_mut().unwrap_or_else(PoisonError::into_inner);
let used = names.len() as u32 * 2;
if let Some(i) = self.current {
if used == 0 {
self.current = None;
return;
}
encoder.resolve_query_set(&self.queries, 0..used, &self.resolve, 0);
encoder.copy_buffer_to_buffer(&self.resolve, 0, &self.slots[i].buffer, 0, u64::from(used) * 8);
self.slots[i].written = used;
let slot = &mut self.slots[i];
slot.names.clear();
slot.names.extend_from_slice(names);
}
}
pub fn submitted(&mut self) {
if let Some(i) = self.current.take() {
let slot = &self.slots[i];
slot.state.store(MAPPING, Ordering::Release);
let state = slot.state.clone();
slot.buffer.map_async(wgpu::MapMode::Read, .., move |result| {
state.store(if result.is_ok() { READY } else { FREE }, Ordering::Release);
});
}
}
}
fn read_frame(
t: &[u64],
names: &[&'static str],
period: f64,
not_before: u64,
spans: &mut Vec<PassSpan>,
order: &mut Vec<(u64, u64, &'static str)>,
) -> Option<(f32, u64)> {
let valid = |&[begin, end]: &[u64; 2]| begin != 0 && begin >= not_before && end >= begin;
let pairs = t.as_chunks::<2>().0;
let (first, last) = pairs
.iter()
.filter(|p| valid(p))
.fold((u64::MAX, 0), |(lo, hi), &[b, e]| (lo.min(b), hi.max(e)));
if last <= first {
return None;
}
let ms = |ticks: u64| (ticks as f64 * period / 1e6) as f32;
spans.clear();
order.clear();
order.extend(
pairs
.iter()
.zip(names)
.filter(|(p, _)| valid(p))
.map(|(&[begin, end], &name)| (begin, end, name)),
);
order.sort_unstable_by_key(|&(begin, _, _)| begin);
let mut done = first;
for &(begin, end, name) in order.iter() {
let start = begin.max(done).min(end);
done = done.max(end);
spans.push(PassSpan {
name,
start_ms: ms(start - first),
ms: ms(end - start),
});
}
Some((ms(last - first), last))
}
fn smooth_passes(passes: &mut Vec<GpuPass>, out: &mut Vec<GpuPass>, frame: &[PassSpan]) {
out.clear();
for span in frame {
match out.iter_mut().find(|p| p.name == span.name) {
Some(p) => p.ms += span.ms,
None => out.push(GpuPass {
name: span.name,
ms: span.ms,
}),
}
}
for p in out.iter_mut() {
if let Some(old) = passes.iter().find(|o| o.name == p.name) {
p.ms = old.ms + (p.ms - old.ms) * 0.1;
}
}
std::mem::swap(passes, out);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frames_split_into_named_passes() {
let mut spans = Vec::new();
let t = [1_000_000, 3_000_000, 3_000_000, 4_000_000, 0, 0];
let (total, _) = read_frame(&t, &["a", "b", "c"], 1.0, 0, &mut spans, &mut Vec::new()).unwrap();
assert!((total - 3.0).abs() < 1e-6);
assert_eq!(spans.len(), 2);
assert_eq!(spans[1].name, "b");
assert!((spans[1].start_ms - 2.0).abs() < 1e-6 && (spans[1].ms - 1.0).abs() < 1e-6);
}
#[test]
fn overlapping_passes_are_charged_from_the_previous_end() {
let mut spans = Vec::new();
let t = [1, 5, 2, 7];
let (total, end) = read_frame(&t, &["a", "b"], 1e6, 0, &mut spans, &mut Vec::new()).unwrap();
assert_eq!(end, 7);
assert_eq!(total, 6.0);
assert_eq!(spans.iter().map(|s| s.ms).sum::<f32>(), total);
assert_eq!((spans[1].start_ms, spans[1].ms), (4.0, 2.0));
}
#[test]
fn stale_timestamps_from_skipped_passes_are_ignored() {
let mut spans = Vec::new();
let t = [10, 14, 1, 2, 14, 16, 15, 4];
let (total, end) = read_frame(&t, &["a", "b", "c", "d"], 1e6, 3, &mut spans, &mut Vec::new()).unwrap();
assert_eq!((total, end), (6.0, 16));
assert_eq!(spans.iter().map(|s| s.name).collect::<Vec<_>>(), ["a", "c"]);
}
#[test]
fn same_named_passes_add_up_and_smooth() {
let span = |name, ms| PassSpan {
name,
start_ms: 0.0,
ms,
};
let (mut passes, mut scratch) = (Vec::new(), Vec::new());
smooth_passes(
&mut passes,
&mut scratch,
&[span("shadows", 1.0), span("opaque", 4.0), span("shadows", 1.0)],
);
assert_eq!(
passes,
[
GpuPass {
name: "shadows",
ms: 2.0
},
GpuPass {
name: "opaque",
ms: 4.0
}
]
);
smooth_passes(&mut passes, &mut scratch, &[span("opaque", 14.0)]);
assert_eq!(passes.len(), 1, "passes that stopped running are dropped");
assert!((passes[0].ms - 5.0).abs() < 1e-5, "{}", passes[0].ms);
}
}