use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use egui_wgpu::wgpu;
const CLOCK_RING: usize = 3;
const QUERY_BYTES: u64 = 16;
pub const PASS_CLOCK_FEATURES: wgpu::Features = wgpu::Features::TIMESTAMP_QUERY
.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS);
enum SlotState {
Free,
Copied,
Mapping,
}
struct ClockSlot {
staging: wgpu::Buffer,
state: SlotState,
mapped: Arc<AtomicBool>,
}
pub struct PassClock {
lane: &'static str,
query_set: wgpu::QuerySet,
resolve_buf: wgpu::Buffer,
period_ns: f32,
slots: [ClockSlot; CLOCK_RING],
armed: Option<usize>,
}
impl PassClock {
#[must_use]
pub fn supported(device: &wgpu::Device) -> bool {
device.features().contains(PASS_CLOCK_FEATURES)
}
fn new(device: &wgpu::Device, queue: &wgpu::Queue, lane: &'static str) -> Self {
let query_set = device.create_query_set(&wgpu::QuerySetDescriptor {
label: Some("lane_pass_clock"),
ty: wgpu::QueryType::Timestamp,
count: 2,
});
let resolve_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("lane_pass_clock_resolve"),
size: QUERY_BYTES,
usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let slots = std::array::from_fn(|_| ClockSlot {
staging: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("lane_pass_clock_staging"),
size: QUERY_BYTES,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
}),
state: SlotState::Free,
mapped: Arc::new(AtomicBool::new(false)),
});
Self { lane, query_set, resolve_buf, period_ns: queue.get_timestamp_period(), slots, armed: None }
}
pub fn begin(&mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder) -> bool {
let _ = device.poll(wgpu::PollType::Poll);
for slot in &mut self.slots {
match slot.state {
SlotState::Mapping if slot.mapped.load(Ordering::Acquire) => {
{
let data = slot.staging.slice(..).get_mapped_range();
let ts: &[u64] = bytemuck::cast_slice(&data);
let delta_ns =
(ts[1].saturating_sub(ts[0]) as f64) * f64::from(self.period_ns);
let us = (delta_ns / 1000.0).ceil() as u64;
if us > 0 {
crate::render::lane::note_gpu_pass(self.lane, us);
}
}
slot.staging.unmap();
slot.state = SlotState::Free;
}
SlotState::Copied => {
let flag = Arc::clone(&slot.mapped);
flag.store(false, Ordering::Release);
slot.staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
if r.is_ok() {
flag.store(true, Ordering::Release);
}
});
slot.state = SlotState::Mapping;
}
_ => {}
}
}
self.armed = self.slots.iter().position(|s| matches!(s.state, SlotState::Free));
if self.armed.is_some() {
encoder.write_timestamp(&self.query_set, 0);
}
self.armed.is_some()
}
pub fn end(&mut self, encoder: &mut wgpu::CommandEncoder) {
let Some(i) = self.armed.take() else { return };
encoder.write_timestamp(&self.query_set, 1);
encoder.resolve_query_set(&self.query_set, 0..2, &self.resolve_buf, 0);
encoder.copy_buffer_to_buffer(&self.resolve_buf, 0, &self.slots[i].staging, 0, QUERY_BYTES);
self.slots[i].state = SlotState::Copied;
}
}
#[derive(Default)]
pub enum PassClockSlot {
#[default]
Untried,
Unavailable,
Ready(PassClock),
}
impl PassClockSlot {
pub fn get_or_init(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
lane: &'static str,
) -> Option<&mut PassClock> {
if matches!(self, PassClockSlot::Untried) {
*self = if PassClock::supported(device) {
crate::render::lane::note_gpu_timestamps(lane, true);
PassClockSlot::Ready(PassClock::new(device, queue, lane))
} else {
crate::render::lane::note_gpu_timestamps(lane, false);
PassClockSlot::Unavailable
};
}
self.ready_mut()
}
pub fn ready_mut(&mut self) -> Option<&mut PassClock> {
match self {
PassClockSlot::Ready(c) => Some(c),
_ => None,
}
}
}