henad_compute/gpu/primitives/
readback.rs1use std::mem::size_of;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum StatsPoll {
12 Pending,
14 Landed,
16 Failed,
18}
19
20#[derive(Debug)]
23pub struct CounterReadback {
24 storage: wgpu::Buffer,
26 staging: wgpu::Buffer,
27 pending: Option<flume::Receiver<Result<(), wgpu::BufferAsyncError>>>,
29 copied: bool,
31 failed: bool,
33 values: Vec<u32>,
34}
35
36impl CounterReadback {
37 pub fn new(device: &wgpu::Device, label: &str, count: usize) -> Self {
41 let size = (count * size_of::<u32>()) as u64;
42 let storage = device.create_buffer(&wgpu::BufferDescriptor {
43 label: Some(&format!("{label}_storage")),
44 size,
45 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
46 mapped_at_creation: false,
47 });
48 let staging = device.create_buffer(&wgpu::BufferDescriptor {
49 label: Some(&format!("{label}_staging")),
50 size,
51 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
52 mapped_at_creation: false,
53 });
54 Self {
55 storage,
56 staging,
57 pending: None,
58 copied: false,
59 failed: false,
60 values: vec![0; count],
61 }
62 }
63
64 fn size(&self) -> u64 {
65 (self.values.len() * size_of::<u32>()) as u64
66 }
67
68 pub fn binding(&self) -> wgpu::BindingResource<'_> {
70 self.storage.as_entire_binding()
71 }
72
73 pub fn encode_clear(&self, encoder: &mut wgpu::CommandEncoder) {
75 encoder.clear_buffer(&self.storage, 0, None);
76 }
77
78 pub fn encode_copy(&mut self, encoder: &mut wgpu::CommandEncoder) {
85 if self.pending.is_some() {
86 return;
87 }
88 encoder.copy_buffer_to_buffer(&self.storage, 0, &self.staging, 0, self.size());
89 self.copied = true;
90 }
91
92 pub fn begin_map(&mut self) {
95 if self.pending.is_some() || !self.copied {
96 return;
97 }
98 self.copied = false;
99 let (tx, rx) = flume::bounded(1);
100 self.staging.slice(..).map_async(wgpu::MapMode::Read, move |result| {
101 drop(tx.send(result));
102 });
103 self.pending = Some(rx);
104 }
105
106 pub fn is_pending(&self) -> bool {
108 self.pending.is_some()
109 }
110
111 pub fn poll(&mut self, device: &wgpu::Device) -> StatsPoll {
117 let Some(rx) = self.pending.as_ref() else {
118 return self.status();
119 };
120
121 drop(device.poll(wgpu::PollType::Poll));
122
123 match rx.try_recv() {
124 Ok(result) => {
125 self.pending = None;
126 self.failed = !self.finish_map(result);
127 }
128 Err(flume::TryRecvError::Disconnected) => self.abandon_map(),
130 Err(flume::TryRecvError::Empty) => {}
131 }
132 self.status()
133 }
134
135 #[cfg(not(target_arch = "wasm32"))]
140 pub fn poll_blocking(&mut self, device: &wgpu::Device) -> StatsPoll {
141 if let Some(rx) = self.pending.as_ref() {
142 let received = match device.poll(wgpu::PollType::wait_indefinitely()) {
143 Ok(_) => rx.recv().ok(),
144 Err(_) => rx.try_recv().ok(),
146 };
147 match received {
148 Some(result) => {
149 self.pending = None;
150 self.failed = !self.finish_map(result);
151 }
152 None => self.abandon_map(),
153 }
154 }
155 self.status()
156 }
157
158 #[cfg(target_arch = "wasm32")]
161 pub fn poll_blocking(&mut self, device: &wgpu::Device) -> StatsPoll {
162 self.poll(device)
163 }
164
165 fn status(&self) -> StatsPoll {
166 if self.pending.is_some() {
167 StatsPoll::Pending
168 } else if self.failed {
169 StatsPoll::Failed
170 } else {
171 StatsPoll::Landed
172 }
173 }
174
175 fn abandon_map(&mut self) {
179 self.pending = None;
180 self.failed = true;
181 self.staging.unmap();
182 }
183
184 fn finish_map(&mut self, result: Result<(), wgpu::BufferAsyncError>) -> bool {
186 if let Err(err) = result {
187 log::warn!("GPU stat readback failed: {err}");
188 return false;
189 }
190
191 let slice = self.staging.slice(..);
192 let data = match slice.get_mapped_range() {
194 Ok(data) => data,
195 Err(err) => {
196 log::warn!("GPU stat readback failed: {err}");
197 self.staging.unmap();
198 return false;
199 }
200 };
201 let words: &[u32] = bytemuck::cast_slice(&data);
202 let n = self.values.len();
203 self.values.copy_from_slice(&words[..n]);
204 drop(data);
205 self.staging.unmap();
206
207 true
208 }
209
210 pub fn values(&self) -> &[u32] {
212 &self.values
213 }
214
215 pub fn values_f32(&self) -> impl Iterator<Item = f32> + '_ {
218 self.values.iter().copied().map(f32::from_bits)
219 }
220}
221
222#[cfg(all(test, not(target_arch = "wasm32")))]
223mod tests {
224 use super::{CounterReadback, StatsPoll};
225 use crate::gpu::{GpuContext, headless_context};
226
227 fn begin_read_back(ctx: &GpuContext, readback: &mut CounterReadback, values: &[u32]) {
229 ctx.queue
230 .write_buffer(&readback.storage, 0, bytemuck::cast_slice(values));
231 let mut encoder = ctx
232 .device
233 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
234 readback.encode_copy(&mut encoder);
235 ctx.queue.submit(Some(encoder.finish()));
236 readback.begin_map();
237 }
238
239 #[test]
240 fn a_completed_map_polls_as_landed() {
241 let Some(ctx) = headless_context("henad_readback_landed_test", wgpu::Features::empty()) else {
242 log::warn!("skipping a_completed_map_polls_as_landed: no adapter");
243 return;
244 };
245 let mut readback = CounterReadback::new(&ctx.device, "henad_readback_landed_test", 3);
246
247 begin_read_back(&ctx, &mut readback, &[1, 2, 3]);
248 assert!(readback.is_pending(), "a begun map is pending");
249 assert_eq!(readback.poll_blocking(&ctx.device), StatsPoll::Landed);
250 assert_eq!(readback.values(), [1, 2, 3]);
251
252 begin_read_back(&ctx, &mut readback, &[4, 5, 6]);
253 ctx.device
254 .poll(wgpu::PollType::wait_indefinitely())
255 .expect("the copy runs");
256 assert_eq!(
257 readback.poll(&ctx.device),
258 StatsPoll::Landed,
259 "a map the device completed lands on the next poll"
260 );
261 assert_eq!(readback.values(), [4, 5, 6]);
262 assert_eq!(
263 readback.poll(&ctx.device),
264 StatsPoll::Landed,
265 "a poll with nothing in flight reports the latest map"
266 );
267 }
268
269 #[test]
270 fn a_failed_map_polls_as_failed_and_keeps_the_values() {
271 let Some(ctx) = headless_context("henad_readback_failed_test", wgpu::Features::empty()) else {
272 log::warn!("skipping a_failed_map_polls_as_failed_and_keeps_the_values: no adapter");
273 return;
274 };
275 let mut readback = CounterReadback::new(&ctx.device, "henad_readback_failed_test", 3);
276 begin_read_back(&ctx, &mut readback, &[1, 2, 3]);
277 assert_eq!(readback.poll_blocking(&ctx.device), StatsPoll::Landed);
278
279 let mut encoder = ctx
281 .device
282 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
283 readback.encode_copy(&mut encoder);
284 ctx.queue.submit(Some(encoder.finish()));
285 readback.staging.destroy();
286 readback.begin_map();
287
288 assert_eq!(readback.poll_blocking(&ctx.device), StatsPoll::Failed);
289 assert_eq!(readback.values(), [1, 2, 3], "a failed map leaves the values it found");
290 assert_eq!(readback.poll(&ctx.device), StatsPoll::Failed);
291 assert!(ctx.faults.take().is_some(), "the device reports the refused map");
292 }
293
294 #[test]
297 fn an_abandoned_map_leaves_the_next_readback_working() {
298 let Some(ctx) = headless_context("henad_readback_abandoned_test", wgpu::Features::empty()) else {
299 log::warn!("skipping an_abandoned_map_leaves_the_next_readback_working: no adapter");
300 return;
301 };
302 let mut readback = CounterReadback::new(&ctx.device, "henad_readback_abandoned_test", 3);
303
304 begin_read_back(&ctx, &mut readback, &[1, 2, 3]);
305 readback.abandon_map();
306 assert_eq!(
307 readback.poll(&ctx.device),
308 StatsPoll::Failed,
309 "an abandoned map polls as failed"
310 );
311
312 begin_read_back(&ctx, &mut readback, &[4, 5, 6]);
314 ctx.device
315 .poll(wgpu::PollType::wait_indefinitely())
316 .expect("the map runs");
317 readback.abandon_map();
318
319 begin_read_back(&ctx, &mut readback, &[7, 8, 9]);
320 assert_eq!(readback.poll_blocking(&ctx.device), StatsPoll::Landed);
321 assert_eq!(readback.values(), [7, 8, 9]);
322 assert!(ctx.faults.take().is_none(), "a copy went into a mapped staging buffer");
323 }
324}