use crate::context::GpuContext;
use crate::error::{GpuError, GpuResult};
const TIMESTAMP_BYTES: u64 = 8;
#[derive(Debug, Clone, PartialEq)]
pub struct PassTiming {
pub label: String,
pub start_ns: u64,
pub end_ns: u64,
pub duration_us: f64,
}
pub struct GpuTimestampProfiler {
enabled: bool,
capacity: u32,
next_slot: u32,
period_ns: f32,
labels: Vec<String>,
query_set: Option<wgpu::QuerySet>,
resolve_buffer: Option<wgpu::Buffer>,
staging_buffer: Option<wgpu::Buffer>,
}
impl std::fmt::Debug for GpuTimestampProfiler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GpuTimestampProfiler")
.field("enabled", &self.enabled)
.field("capacity", &self.capacity)
.field("next_slot", &self.next_slot)
.field("period_ns", &self.period_ns)
.field("labels", &self.labels)
.field("has_query_set", &self.query_set.is_some())
.field("has_resolve_buffer", &self.resolve_buffer.is_some())
.field("has_staging_buffer", &self.staging_buffer.is_some())
.finish()
}
}
impl GpuTimestampProfiler {
pub fn try_new(ctx: &GpuContext, capacity: u32) -> Option<Self> {
let features = ctx.device().features();
if !features.contains(wgpu::Features::TIMESTAMP_QUERY) {
return None;
}
if !features.contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS) {
return None;
}
let capacity = capacity.max(2);
let capacity = if capacity % 2 == 0 {
capacity
} else {
capacity.saturating_add(1)
};
let period_ns = ctx.queue().get_timestamp_period();
let query_set = ctx.device().create_query_set(&wgpu::QuerySetDescriptor {
label: Some("oxigdal_profiler_timestamps"),
count: capacity,
ty: wgpu::QueryType::Timestamp,
});
let byte_size = (capacity as u64).saturating_mul(TIMESTAMP_BYTES);
let resolve_buffer = ctx.device().create_buffer(&wgpu::BufferDescriptor {
label: Some("oxigdal_profiler_resolve"),
size: byte_size,
usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging_buffer = ctx.device().create_buffer(&wgpu::BufferDescriptor {
label: Some("oxigdal_profiler_staging"),
size: byte_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Some(Self {
enabled: true,
capacity,
next_slot: 0,
period_ns,
labels: Vec::new(),
query_set: Some(query_set),
resolve_buffer: Some(resolve_buffer),
staging_buffer: Some(staging_buffer),
})
}
pub fn dummy(period_ns: f32) -> Self {
Self {
enabled: false,
capacity: 0,
next_slot: 0,
period_ns,
labels: Vec::new(),
query_set: None,
resolve_buffer: None,
staging_buffer: None,
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn period_ns(&self) -> f32 {
self.period_ns
}
pub fn capacity(&self) -> u32 {
self.capacity
}
pub fn next_slot(&self) -> u32 {
self.next_slot
}
pub fn begin_pass(&mut self, encoder: &mut wgpu::CommandEncoder, label: &str) -> Option<u32> {
if !self.enabled {
return None;
}
if self.next_slot.saturating_add(1) >= self.capacity {
return None;
}
let qs = self.query_set.as_ref()?;
let slot = self.next_slot;
encoder.write_timestamp(qs, slot);
self.labels.push(label.to_string());
self.next_slot = self.next_slot.saturating_add(2);
Some(slot)
}
pub fn end_pass(&self, encoder: &mut wgpu::CommandEncoder, start_slot: u32) {
if !self.enabled {
return;
}
if let Some(qs) = &self.query_set {
encoder.write_timestamp(qs, start_slot.saturating_add(1));
}
}
pub fn resolve(&mut self, ctx: &GpuContext) -> GpuResult<Vec<PassTiming>> {
if !self.enabled || self.next_slot == 0 {
return Ok(Vec::new());
}
let (Some(qs), Some(resolve_buf), Some(staging_buf)) = (
self.query_set.as_ref(),
self.resolve_buffer.as_ref(),
self.staging_buffer.as_ref(),
) else {
return Ok(Vec::new());
};
let recorded_slots = self.next_slot;
let recorded_bytes = (recorded_slots as u64).saturating_mul(TIMESTAMP_BYTES);
let mut encoder = ctx
.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("oxigdal_profiler_resolve_encoder"),
});
encoder.resolve_query_set(qs, 0..recorded_slots, resolve_buf, 0);
encoder.copy_buffer_to_buffer(resolve_buf, 0, staging_buf, 0, recorded_bytes);
ctx.queue().submit([encoder.finish()]);
let slice = staging_buf.slice(0..recorded_bytes);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
ctx.device()
.poll(wgpu::PollType::wait_indefinitely())
.map_err(|e| {
GpuError::execution_failed(format!(
"profiler: device poll while mapping staging buffer failed: {e:?}"
))
})?;
rx.recv()
.map_err(|e| {
GpuError::execution_failed(format!(
"profiler: mapping callback channel closed: {e}"
))
})?
.map_err(|e| {
GpuError::execution_failed(format!("profiler: map_async failed: {e:?}"))
})?;
let data = slice.get_mapped_range();
let raw_ts: Vec<u64> = data
.chunks_exact(TIMESTAMP_BYTES as usize)
.map(|c| u64::from_ne_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
.collect();
drop(data);
staging_buf.unmap();
let mut out = Vec::with_capacity(self.labels.len());
for (i, label) in self.labels.iter().enumerate() {
let start_raw = raw_ts.get(i * 2).copied().unwrap_or(0);
let end_raw = raw_ts.get(i * 2 + 1).copied().unwrap_or(0);
let duration_ns = end_raw.saturating_sub(start_raw) as f64 * self.period_ns as f64;
let start_ns = (start_raw as f64 * self.period_ns as f64) as u64;
let end_ns = (end_raw as f64 * self.period_ns as f64) as u64;
out.push(PassTiming {
label: label.clone(),
start_ns,
end_ns,
duration_us: duration_ns / 1000.0,
});
}
Ok(out)
}
pub fn pass_labels(&self) -> &[String] {
&self.labels
}
pub fn reset(&mut self) {
self.labels.clear();
self.next_slot = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dummy_profiler_has_no_query_set() {
let prof = GpuTimestampProfiler::dummy(1.0);
assert!(!prof.is_enabled());
assert_eq!(prof.capacity(), 0);
assert_eq!(prof.next_slot(), 0);
assert_eq!(prof.pass_labels().len(), 0);
assert!(prof.query_set.is_none());
assert!(prof.resolve_buffer.is_none());
assert!(prof.staging_buffer.is_none());
}
#[test]
fn dummy_profiler_reset_is_idempotent() {
let mut prof = GpuTimestampProfiler::dummy(2.0);
prof.reset();
prof.reset();
assert_eq!(prof.next_slot(), 0);
assert!(prof.pass_labels().is_empty());
}
#[test]
fn pass_timing_construction() {
let t = PassTiming {
label: "blur".to_string(),
start_ns: 1_000,
end_ns: 2_500,
duration_us: 1.5,
};
assert_eq!(t.label, "blur");
assert_eq!(t.end_ns - t.start_ns, 1_500);
assert!((t.duration_us - 1.5).abs() < f64::EPSILON);
}
#[test]
fn debug_impl_does_not_panic() {
let prof = GpuTimestampProfiler::dummy(1.0);
let s = format!("{:?}", prof);
assert!(s.contains("GpuTimestampProfiler"));
}
}