1use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
10use std::sync::{Arc, Mutex};
11
12const SAMPLE_EVERY: u64 = 8;
14const RING: usize = 4;
16const RESOLVED_BYTES: u64 = 3 * 8;
17
18#[derive(Clone, Copy, Debug, Default, PartialEq)]
20pub struct FrameGpuTimes {
21 pub render_ms: f64,
23 pub pack_ms: f64,
25 pub sampled_frames: u64,
27 pub flat_frames: u64,
30}
31
32impl FrameGpuTimes {
33 #[must_use]
35 #[allow(clippy::cast_precision_loss, reason = "a frame count far below 2^52")]
36 pub fn render_ms_per_frame(&self) -> Option<f64> {
37 (self.sampled_frames > 0).then(|| self.render_ms / self.sampled_frames as f64)
38 }
39
40 #[must_use]
42 #[allow(clippy::cast_precision_loss, reason = "a frame count far below 2^52")]
43 pub fn pack_ms_per_frame(&self) -> Option<f64> {
44 (self.sampled_frames > 0).then(|| self.pack_ms / self.sampled_frames as f64)
45 }
46}
47
48#[derive(Debug)]
49struct Slot {
50 resolve: wgpu::Buffer,
51 readback: wgpu::Buffer,
52 pending: AtomicBool,
53 mapped: Arc<AtomicU8>,
55}
56
57const MAP_WAITING: u8 = 0;
58const MAP_DONE: u8 = 1;
59const MAP_FAILED: u8 = 2;
60
61#[derive(Debug)]
62pub(crate) struct FrameGpuTimer {
63 queries: wgpu::QuerySet,
64 slots: Vec<Slot>,
65 frames: AtomicU64,
66 period: f32,
68 totals: Mutex<FrameGpuTimes>,
69}
70
71pub(crate) struct FrameSample<'a> {
72 timer: &'a FrameGpuTimer,
73 slot: usize,
74}
75
76impl FrameGpuTimer {
77 pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
79 let queries = device.create_query_set(&wgpu::QuerySetDescriptor {
80 label: Some("alpha2 frame GPU time"),
81 ty: wgpu::QueryType::Timestamp,
82 count: 3,
83 });
84 let slots = (0..RING)
85 .map(|_| Slot {
86 resolve: device.create_buffer(&wgpu::BufferDescriptor {
87 label: Some("alpha2 frame GPU time resolve"),
88 size: RESOLVED_BYTES,
89 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
90 mapped_at_creation: false,
91 }),
92 readback: device.create_buffer(&wgpu::BufferDescriptor {
93 label: Some("alpha2 frame GPU time readback"),
94 size: RESOLVED_BYTES,
95 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
96 mapped_at_creation: false,
97 }),
98 pending: AtomicBool::new(false),
99 mapped: Arc::new(AtomicU8::new(MAP_WAITING)),
100 })
101 .collect();
102 Self {
103 queries,
104 slots,
105 frames: AtomicU64::new(0),
106 period: queue.get_timestamp_period(),
107 totals: Mutex::default(),
108 }
109 }
110
111 #[allow(
112 clippy::cast_precision_loss,
113 reason = "a frame's span in timestamp ticks, far below 2^52"
114 )]
115 fn collect(&self) {
116 let mut totals = self
117 .totals
118 .lock()
119 .unwrap_or_else(std::sync::PoisonError::into_inner);
120 for slot in &self.slots {
121 if !slot.pending.load(Ordering::Acquire) {
122 continue;
123 }
124 match slot.mapped.load(Ordering::Acquire) {
125 MAP_WAITING => continue,
126 MAP_FAILED => {
128 slot.mapped.store(MAP_WAITING, Ordering::Release);
129 slot.pending.store(false, Ordering::Release);
130 continue;
131 }
132 _ => {}
133 }
134 if let Ok(bytes) = slot.readback.slice(..).get_mapped_range() {
135 let tick = |at: usize| {
136 let mut word = [0; 8];
137 word.copy_from_slice(&bytes[at * 8..at * 8 + 8]);
138 u64::from_le_bytes(word)
139 };
140 let (start, graph_end, pack_end) = (tick(0), tick(1), tick(2));
141 if start == pack_end {
143 totals.flat_frames += 1;
144 } else if start <= graph_end && graph_end <= pack_end {
145 let milliseconds =
146 |ticks: u64| ticks as f64 * f64::from(self.period) / 1_000_000.0;
147 totals.render_ms += milliseconds(graph_end - start);
148 totals.pack_ms += milliseconds(pack_end - graph_end);
149 totals.sampled_frames += 1;
150 }
151 }
152 slot.readback.unmap();
153 slot.mapped.store(MAP_WAITING, Ordering::Release);
154 slot.pending.store(false, Ordering::Release);
155 }
156 }
157
158 pub(crate) fn begin(&self) -> Option<FrameSample<'_>> {
160 let frame = self.frames.fetch_add(1, Ordering::Relaxed);
161 if !frame.is_multiple_of(SAMPLE_EVERY) {
162 return None;
163 }
164 self.collect();
165 let slot = self
166 .slots
167 .iter()
168 .position(|slot| !slot.pending.load(Ordering::Acquire))?;
169 Some(FrameSample { timer: self, slot })
170 }
171
172 pub(crate) fn take(&self) -> FrameGpuTimes {
174 self.collect();
175 std::mem::take(
176 &mut *self
177 .totals
178 .lock()
179 .unwrap_or_else(std::sync::PoisonError::into_inner),
180 )
181 }
182}
183
184impl FrameSample<'_> {
185 fn mark(&self, encoder: &mut wgpu::CommandEncoder, index: u32) {
188 let writes = if index == 0 {
189 wgpu::ComputePassTimestampWrites {
190 query_set: &self.timer.queries,
191 beginning_of_pass_write_index: None,
192 end_of_pass_write_index: Some(index),
193 }
194 } else {
195 wgpu::ComputePassTimestampWrites {
196 query_set: &self.timer.queries,
197 beginning_of_pass_write_index: Some(index),
198 end_of_pass_write_index: None,
199 }
200 };
201 drop(encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
202 label: Some("alpha2 frame GPU time mark"),
203 timestamp_writes: Some(writes),
204 }));
205 }
206
207 pub(crate) fn graph_start(&self, encoder: &mut wgpu::CommandEncoder) {
208 self.mark(encoder, 0);
209 }
210
211 pub(crate) fn graph_end(&self, encoder: &mut wgpu::CommandEncoder) {
212 self.mark(encoder, 1);
213 }
214
215 pub(crate) fn pack_end(&self, encoder: &mut wgpu::CommandEncoder) {
217 self.mark(encoder, 2);
218 let slot = &self.timer.slots[self.slot];
219 encoder.resolve_query_set(&self.timer.queries, 0..3, &slot.resolve, 0);
220 encoder.copy_buffer_to_buffer(&slot.resolve, 0, &slot.readback, 0, RESOLVED_BYTES);
221 }
222
223 pub(crate) fn submitted(self, gpu: &alpha2_gpu::GpuContext) {
225 let slot = &self.timer.slots[self.slot];
226 slot.pending.store(true, Ordering::Release);
227 let mapped = Arc::clone(&slot.mapped);
228 gpu.map_async(
229 &slot.readback.slice(..),
230 wgpu::MapMode::Read,
231 move |result| {
232 mapped.store(
233 if result.is_ok() { MAP_DONE } else { MAP_FAILED },
234 Ordering::Release,
235 );
236 },
237 );
238 }
239}