1use std::{
2 cell::{Cell, RefCell},
3 sync::{
4 Arc,
5 atomic::{AtomicU8, Ordering},
6 },
7};
8
9use crate::debug_toggles::DebugToggle;
10
11const QUERY_CAPACITY: u32 = 512;
12
13const READBACK_SLOTS: usize = 4;
14
15const SLOT_FREE: u8 = 0;
16const SLOT_PENDING: u8 = 1;
17const SLOT_MAPPED: u8 = 2;
18const SLOT_FAILED: u8 = 3;
19
20const PRINT_CADENCE_FRAMES: u64 = 60;
21
22static PASS_TIMING: DebugToggle = DebugToggle::new("CRANPOSE_GPU_PASS_TIMING");
23
24pub(crate) fn pass_timing_requested() -> bool {
25 PASS_TIMING.flag()
26}
27
28#[derive(Clone, Debug, PartialEq)]
30pub struct GpuPassTimingEntry {
31 pub label: String,
32 pub total_ms: f64,
33 pub passes: u64,
34}
35
36#[derive(Clone, Debug, Default, PartialEq)]
38pub struct GpuPassTimingReport {
39 pub frames: u32,
41 pub span_ms: f64,
46 pub entries: Vec<GpuPassTimingEntry>,
48}
49
50#[derive(Clone, Copy, Default)]
51struct LabelTotal {
52 nanoseconds: u64,
53 passes: u64,
54}
55
56struct ReadbackSlot {
57 buffer: wgpu::Buffer,
58 state: Arc<AtomicU8>,
59 passes: RefCell<Vec<(u16, u32)>>,
60}
61
62pub(crate) struct PassTimer {
63 query_set: wgpu::QuerySet,
64 resolve_buffer: wgpu::Buffer,
65 period_ns: f32,
66 cursor: Cell<u32>,
67 frame_passes: RefCell<Vec<(u16, u32)>>,
68 labels: RefCell<Vec<String>>,
69 totals: RefCell<Vec<LabelTotal>>,
70 slots: Vec<ReadbackSlot>,
71 frame_index: Cell<u64>,
72 frames_harvested: Cell<u32>,
73 span_nanoseconds: Cell<u64>,
74 dropped_passes: Cell<u64>,
75 dropped_frames: Cell<u64>,
76}
77
78impl PassTimer {
79 pub(crate) fn for_device(device: &wgpu::Device, queue: &wgpu::Queue) -> Option<Self> {
80 if !device.features().contains(wgpu::Features::TIMESTAMP_QUERY) {
81 eprintln!(
82 "[GPU-PASS] CRANPOSE_GPU_PASS_TIMING is set but the adapter lacks TIMESTAMP_QUERY; passes will not be timed"
83 );
84 return None;
85 }
86 let query_set = device.create_query_set(&wgpu::QuerySetDescriptor {
87 label: Some("Pass Timing Query Set"),
88 ty: wgpu::QueryType::Timestamp,
89 count: QUERY_CAPACITY,
90 });
91 let buffer_size = u64::from(QUERY_CAPACITY) * u64::from(wgpu::QUERY_SIZE);
92 let resolve_buffer = device.create_buffer(&wgpu::BufferDescriptor {
93 label: Some("Pass Timing Resolve Buffer"),
94 size: buffer_size,
95 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
96 mapped_at_creation: false,
97 });
98 let slots = (0..READBACK_SLOTS)
99 .map(|_| ReadbackSlot {
100 buffer: device.create_buffer(&wgpu::BufferDescriptor {
101 label: Some("Pass Timing Readback Buffer"),
102 size: buffer_size,
103 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
104 mapped_at_creation: false,
105 }),
106 state: Arc::new(AtomicU8::new(SLOT_FREE)),
107 passes: RefCell::new(Vec::new()),
108 })
109 .collect();
110 Some(Self {
111 query_set,
112 resolve_buffer,
113 period_ns: queue.get_timestamp_period(),
114 cursor: Cell::new(0),
115 frame_passes: RefCell::new(Vec::new()),
116 labels: RefCell::new(Vec::new()),
117 totals: RefCell::new(Vec::new()),
118 slots,
119 frame_index: Cell::new(0),
120 frames_harvested: Cell::new(0),
121 span_nanoseconds: Cell::new(0),
122 dropped_passes: Cell::new(0),
123 dropped_frames: Cell::new(0),
124 })
125 }
126
127 pub(crate) fn query_set(&self) -> &wgpu::QuerySet {
128 &self.query_set
129 }
130
131 pub(crate) fn begin_pass(&self, label: &str) -> Option<(u32, u32)> {
132 let begin = self.cursor.get();
133 if begin + 2 > QUERY_CAPACITY {
134 self.dropped_passes.set(self.dropped_passes.get() + 1);
135 return None;
136 }
137 self.cursor.set(begin + 2);
138 let label_id = self.intern(label);
139 self.frame_passes.borrow_mut().push((label_id, begin));
140 Some((begin, begin + 1))
141 }
142
143 fn intern(&self, label: &str) -> u16 {
144 let mut labels = self.labels.borrow_mut();
145 if let Some(id) = labels.iter().position(|known| known == label) {
146 return id as u16;
147 }
148 labels.push(label.to_string());
149 self.totals.borrow_mut().push(LabelTotal::default());
150 (labels.len() - 1) as u16
151 }
152
153 pub(crate) fn harvest_completed(&self) {
154 for slot in &self.slots {
155 match slot.state.load(Ordering::Acquire) {
156 SLOT_MAPPED => {
157 match slot.buffer.slice(..).get_mapped_range() {
158 Ok(mapped) => {
159 let span = accumulate_frame(
160 &mut self.totals.borrow_mut(),
161 &slot.passes.borrow(),
162 &mapped,
163 self.period_ns,
164 );
165 self.span_nanoseconds
166 .set(self.span_nanoseconds.get().saturating_add(span));
167 }
168 Err(error) => log::debug!("pass timings could not be read: {error}"),
169 }
170 slot.buffer.unmap();
171 slot.passes.borrow_mut().clear();
172 slot.state.store(SLOT_FREE, Ordering::Release);
173 self.frames_harvested.set(self.frames_harvested.get() + 1);
174 }
175 SLOT_FAILED => {
176 slot.passes.borrow_mut().clear();
177 slot.state.store(SLOT_FREE, Ordering::Release);
178 }
179 _ => {}
180 }
181 }
182 }
183
184 pub(crate) fn frame_resolve(&self) -> Option<PendingResolve<'_>> {
185 let used = self.cursor.get();
186 if used == 0 {
187 return None;
188 }
189 let Some(slot_index) = self
190 .slots
191 .iter()
192 .position(|slot| slot.state.load(Ordering::Acquire) == SLOT_FREE)
193 else {
194 self.dropped_frames.set(self.dropped_frames.get() + 1);
195 return None;
196 };
197 Some(PendingResolve {
198 timer: self,
199 slot_index,
200 used,
201 })
202 }
203
204 pub(crate) fn finish_frame(&self) {
205 self.cursor.set(0);
206 self.frame_passes.borrow_mut().clear();
207 let frame = self.frame_index.get() + 1;
208 self.frame_index.set(frame);
209 if frame.is_multiple_of(PRINT_CADENCE_FRAMES) {
210 self.print_and_reset_window(frame);
211 }
212 }
213
214 pub(crate) fn report(&self) -> GpuPassTimingReport {
215 let labels = self.labels.borrow();
216 let totals = self.totals.borrow();
217 let mut entries: Vec<GpuPassTimingEntry> = labels
218 .iter()
219 .zip(totals.iter())
220 .filter(|(_, total)| total.passes > 0)
221 .map(|(label, total)| GpuPassTimingEntry {
222 label: label.clone(),
223 total_ms: total.nanoseconds as f64 / 1_000_000.0,
224 passes: total.passes,
225 })
226 .collect();
227 entries.sort_by(|a, b| b.total_ms.total_cmp(&a.total_ms));
228 GpuPassTimingReport {
229 frames: self.frames_harvested.get(),
230 span_ms: self.span_nanoseconds.get() as f64 / 1_000_000.0,
231 entries,
232 }
233 }
234
235 fn print_and_reset_window(&self, frame: u64) {
236 let report = self.report();
237 if report.frames > 0 {
238 let frames = f64::from(report.frames);
239 let total_ms: f64 = report.entries.iter().map(|entry| entry.total_ms).sum();
240 let mut line = format!(
241 "[GPU-PASS f#{frame}] frames={} span={:.2}ms/frame occupancy={:.2}ms/frame",
242 report.frames,
243 report.span_ms / frames,
244 total_ms / frames,
245 );
246 for entry in &report.entries {
247 line.push_str(&format!(
248 " | {} {:.2}ms x{:.1}",
249 entry.label,
250 entry.total_ms / frames,
251 entry.passes as f64 / frames,
252 ));
253 }
254 if self.dropped_passes.get() > 0 || self.dropped_frames.get() > 0 {
255 line.push_str(&format!(
256 " | dropped: passes={} frames={}",
257 self.dropped_passes.get(),
258 self.dropped_frames.get(),
259 ));
260 }
261 eprintln!("{line}");
262 }
263 for total in self.totals.borrow_mut().iter_mut() {
264 *total = LabelTotal::default();
265 }
266 self.frames_harvested.set(0);
267 self.span_nanoseconds.set(0);
268 self.dropped_passes.set(0);
269 self.dropped_frames.set(0);
270 }
271}
272
273pub(crate) struct PendingResolve<'timer> {
274 timer: &'timer PassTimer,
275 slot_index: usize,
276 used: u32,
277}
278
279impl PendingResolve<'_> {
280 pub(crate) fn encode(&self, encoder: &mut wgpu::CommandEncoder) {
281 let slot = &self.timer.slots[self.slot_index];
282 encoder.resolve_query_set(
283 &self.timer.query_set,
284 0..self.used,
285 &self.timer.resolve_buffer,
286 0,
287 );
288 encoder.copy_buffer_to_buffer(
289 &self.timer.resolve_buffer,
290 0,
291 &slot.buffer,
292 0,
293 u64::from(self.used) * u64::from(wgpu::QUERY_SIZE),
294 );
295 }
296
297 pub(crate) fn arm_readback(self) {
298 let slot = &self.timer.slots[self.slot_index];
299 slot.passes
300 .borrow_mut()
301 .clone_from(&self.timer.frame_passes.borrow());
302 slot.state.store(SLOT_PENDING, Ordering::Release);
303 let state = Arc::clone(&slot.state);
304 slot.buffer
305 .slice(..)
306 .map_async(wgpu::MapMode::Read, move |result| {
307 let outcome = if result.is_ok() {
308 SLOT_MAPPED
309 } else {
310 SLOT_FAILED
311 };
312 state.store(outcome, Ordering::Release);
313 });
314 }
315}
316
317pub(crate) fn begin_timed_render_pass<'encoder>(
318 pass_timer: Option<&PassTimer>,
319 encoder: &'encoder mut wgpu::CommandEncoder,
320 descriptor: &wgpu::RenderPassDescriptor<'_>,
321) -> wgpu::RenderPass<'encoder> {
322 crate::frame_graph::note_render_pass(descriptor);
323 let timing = pass_timer.and_then(|timer| {
324 timer
325 .begin_pass(descriptor.label.unwrap_or("<unlabeled pass>"))
326 .map(|(begin, end)| (timer, begin, end))
327 });
328 let timestamp_writes = timing.map(|(timer, begin, end)| wgpu::RenderPassTimestampWrites {
329 query_set: timer.query_set(),
330 beginning_of_pass_write_index: Some(begin),
331 end_of_pass_write_index: Some(end),
332 });
333 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
334 timestamp_writes,
335 ..descriptor.clone()
336 })
337}
338
339fn accumulate_frame(
340 totals: &mut [LabelTotal],
341 passes: &[(u16, u32)],
342 mapped: &[u8],
343 period_ns: f32,
344) -> u64 {
345 let read_tick = |index: u32| -> Option<u64> {
346 let offset = index as usize * 8;
347 let bytes = mapped.get(offset..offset + 8)?;
348 Some(u64::from_le_bytes(bytes.try_into().expect("8-byte slice")))
349 };
350 let mut first_begin = u64::MAX;
351 let mut last_end = 0u64;
352 for &(label_id, begin_index) in passes {
353 let Some(total) = totals.get_mut(usize::from(label_id)) else {
354 continue;
355 };
356 let (Some(begin), Some(end)) = (read_tick(begin_index), read_tick(begin_index + 1)) else {
357 continue;
358 };
359 if end < begin {
360 continue;
361 }
362 first_begin = first_begin.min(begin);
363 last_end = last_end.max(end);
364 total.nanoseconds = total
365 .nanoseconds
366 .saturating_add(((end - begin) as f64 * f64::from(period_ns)) as u64);
367 total.passes += 1;
368 }
369 if last_end <= first_begin {
370 return 0;
371 }
372 ((last_end - first_begin) as f64 * f64::from(period_ns)) as u64
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 fn ticks(values: &[u64]) -> Vec<u8> {
380 values.iter().flat_map(|v| v.to_le_bytes()).collect()
381 }
382
383 #[test]
384 fn accumulate_frame_attributes_ticks_by_label() {
385 let mut totals = vec![LabelTotal::default(); 2];
386 let mapped = ticks(&[1_000, 1_100, 1_100, 1_150, 1_150, 1_160]);
387 accumulate_frame(&mut totals, &[(0, 0), (1, 2), (0, 4)], &mapped, 2.0);
388 assert_eq!(totals[0].nanoseconds, 220);
389 assert_eq!(totals[0].passes, 2);
390 assert_eq!(totals[1].nanoseconds, 100);
391 assert_eq!(totals[1].passes, 1);
392 }
393
394 #[test]
395 fn accumulate_frame_skips_backwards_and_out_of_range_pairs() {
396 let mut totals = vec![LabelTotal::default(); 1];
397 let mapped = ticks(&[500, 400]);
398 accumulate_frame(&mut totals, &[(0, 0)], &mapped, 1.0);
399 assert_eq!(totals[0].passes, 0, "an end before its begin is skipped");
400
401 accumulate_frame(&mut totals, &[(0, 6)], &mapped, 1.0);
402 assert_eq!(totals[0].passes, 0, "indices past the mapping are skipped");
403
404 let mapped = ticks(&[100, 250]);
405 accumulate_frame(&mut totals, &[(9, 0)], &mapped, 1.0);
406 assert_eq!(totals[0].passes, 0, "an unknown label id is skipped");
407 }
408}