1use std::{
2 any::type_name,
3 marker::PhantomData,
4 sync::{
5 Arc, Mutex,
6 atomic::{AtomicBool, Ordering},
7 },
8};
9
10use bevy::{
11 core_pipeline::schedule::camera_driver,
12 prelude::*,
13 render::{
14 Render, RenderApp, RenderSystems,
15 render_asset::RenderAssets,
16 render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode},
17 renderer::{RenderContext, RenderDevice, RenderGraph, RenderGraphSystems},
18 storage::GpuShaderBuffer,
19 },
20};
21use encase::{
22 ShaderSize, ShaderType,
23 internal::{CreateFrom, WriteInto},
24};
25
26use crate::{
27 ArrayBufferChanges, ShaderPassSystems, ShaderSystems,
28 auto_array::{ArrayBufferState, array_stride},
29 bindings::AutoBufferKind,
30 cpu_buffer::CpuBuffer,
31 gpu_buffer::GpuBuffer,
32};
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum ReadbackMode {
37 EveryFrame,
40 OnRequest,
42}
43
44#[derive(Default)]
47struct ReadbackShared {
48 requested: AtomicBool,
50 in_flight: AtomicBool,
52 result: Mutex<Option<Vec<u8>>>,
54}
55
56impl ReadbackShared {
57 fn take_result(&self) -> Option<Vec<u8>> {
58 self.result.lock().ok()?.take()
59 }
60}
61
62#[derive(Resource)]
84pub struct GpuReadback<Tag, T> {
85 shared: Arc<ReadbackShared>,
86 latest: Option<Vec<T>>,
87 _marker: PhantomData<Tag>,
88}
89
90impl<Tag, T> GpuReadback<Tag, T> {
91 pub fn latest(&self) -> Option<&[T]> {
94 self.latest.as_deref()
95 }
96
97 pub fn request(&self) {
101 self.shared.requested.store(true, Ordering::Release);
102 }
103}
104
105#[derive(Resource)]
112pub struct StorageReadback<S> {
113 shared: Arc<ReadbackShared>,
114 latest: Option<S>,
115}
116
117impl<S> StorageReadback<S> {
118 pub fn latest(&self) -> Option<&S> {
121 self.latest.as_ref()
122 }
123
124 pub fn request(&self) {
128 self.shared.requested.store(true, Ordering::Release);
129 }
130}
131
132struct QueuedCopy {
138 source: Buffer,
139 staging: Buffer,
140 size: u64,
141 shared: Arc<ReadbackShared>,
142}
143
144#[derive(Resource, Default)]
147struct ReadbackCopies(Vec<QueuedCopy>);
148
149#[derive(Resource)]
151struct ReadbackJob<Key> {
152 shared: Arc<ReadbackShared>,
153 mode: ReadbackMode,
154 staging: Option<Buffer>,
155 _marker: PhantomData<Key>,
156}
157
158struct GpuKey<Tag>(PhantomData<Tag>);
160struct ArrayKey<Tag>(PhantomData<Tag>);
161struct StorageKey<S>(PhantomData<S>);
162
163fn should_copy(mode: ReadbackMode, in_flight: bool, requested: &AtomicBool) -> bool {
166 if in_flight {
167 return false;
168 }
169 match mode {
170 ReadbackMode::EveryFrame => true,
171 ReadbackMode::OnRequest => requested.swap(false, Ordering::AcqRel),
172 }
173}
174
175impl<Key> ReadbackJob<Key> {
176 fn new(shared: Arc<ReadbackShared>, mode: ReadbackMode) -> Self {
177 Self {
178 shared,
179 mode,
180 staging: None,
181 _marker: PhantomData,
182 }
183 }
184
185 fn queue_copy(
187 &mut self,
188 source: &Buffer,
189 size: u64,
190 render_device: &RenderDevice,
191 copies: &mut ReadbackCopies,
192 ) {
193 if size == 0
194 || !should_copy(
195 self.mode,
196 self.shared.in_flight.load(Ordering::Acquire),
197 &self.shared.requested,
198 )
199 {
200 return;
201 }
202
203 let staging = match &self.staging {
206 Some(staging) if staging.size() == size => staging.clone(),
207 _ => {
208 let staging = render_device.create_buffer(&BufferDescriptor {
209 label: Some("readback_staging_buffer"),
210 size,
211 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
212 mapped_at_creation: false,
213 });
214 self.staging = Some(staging.clone());
215 staging
216 }
217 };
218
219 self.shared.in_flight.store(true, Ordering::Release);
220 copies.0.push(QueuedCopy {
221 source: source.clone(),
222 staging,
223 size,
224 shared: self.shared.clone(),
225 });
226 }
227}
228
229fn readback_copy_pass(copies: Res<ReadbackCopies>, mut ctx: RenderContext) {
234 if copies.0.is_empty() {
235 return;
236 }
237 let encoder = ctx.command_encoder();
238 for copy in &copies.0 {
239 encoder.copy_buffer_to_buffer(©.source, 0, ©.staging, 0, copy.size);
240 }
241}
242
243fn map_readbacks(mut copies: ResMut<ReadbackCopies>) {
246 for copy in copies.0.drain(..) {
247 let QueuedCopy {
248 staging,
249 size,
250 shared,
251 ..
252 } = copy;
253 let mapped = staging.clone();
254 staging
255 .slice(..size)
256 .map_async(MapMode::Read, move |result| {
257 match result {
258 Ok(()) => {
259 let bytes = mapped.slice(..size).get_mapped_range().to_vec();
260 mapped.unmap();
261 if let Ok(mut slot) = shared.result.lock() {
262 *slot = Some(bytes);
263 }
264 }
265 Err(error) => warn!("buffer readback failed: {error}"),
266 }
267 shared.in_flight.store(false, Ordering::Release);
268 });
269 }
270}
271
272struct ReadbackPlugin;
274
275impl Plugin for ReadbackPlugin {
276 fn build(&self, app: &mut App) {
277 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
278 return;
279 };
280 render_app
281 .init_resource::<ReadbackCopies>()
282 .add_systems(
283 RenderGraph,
284 readback_copy_pass
285 .in_set(ShaderPassSystems::ReadbackCopy)
286 .in_set(RenderGraphSystems::Render)
287 .after(camera_driver),
288 )
289 .add_systems(Render, map_readbacks.in_set(RenderSystems::Cleanup));
292 }
293}
294
295fn add_readback_plugin(app: &mut App) {
296 if !app.is_plugin_added::<ReadbackPlugin>() {
297 app.add_plugins(ReadbackPlugin);
298 }
299}
300
301fn add_render_job<Key, M>(
304 app: &mut App,
305 shared: Arc<ReadbackShared>,
306 mode: ReadbackMode,
307 queue_system: impl IntoScheduleConfigs<bevy::ecs::system::ScheduleSystem, M>,
308) where
309 Key: Send + Sync + 'static,
310{
311 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
312 return;
313 };
314 render_app
315 .insert_resource(ReadbackJob::<Key>::new(shared, mode))
316 .add_systems(
317 Render,
318 queue_system
319 .in_set(RenderSystems::PrepareBindGroups)
320 .after(ShaderSystems::PrepareBindings),
321 );
322}
323
324fn decode<D: ShaderType + CreateFrom>(bytes: &[u8]) -> Option<D> {
325 match encase::StorageBuffer::new(bytes).create() {
326 Ok(value) => Some(value),
327 Err(error) => {
328 error!("could not decode read-back {}: {error}", type_name::<D>());
329 None
330 }
331 }
332}
333
334fn deliver_slice<Tag, T>(mut readback: ResMut<GpuReadback<Tag, T>>)
336where
337 Tag: Send + Sync + 'static,
338 T: ShaderSize + CreateFrom + Send + Sync + 'static,
339{
340 if let Some(bytes) = readback.shared.take_result()
341 && let Some(values) = decode::<Vec<T>>(&bytes)
342 {
343 readback.latest = Some(values);
344 }
345}
346
347fn deliver_storage<S>(mut readback: ResMut<StorageReadback<S>>)
349where
350 S: ShaderType + CreateFrom + Send + Sync + 'static,
351{
352 if let Some(bytes) = readback.shared.take_result()
353 && let Some(value) = decode::<S>(&bytes)
354 {
355 readback.latest = Some(value);
356 }
357}
358
359fn insert_slice_readback<Tag, T>(app: &mut App) -> Arc<ReadbackShared>
361where
362 Tag: Send + Sync + 'static,
363 T: ShaderSize + CreateFrom + Send + Sync + 'static,
364{
365 assert!(
366 !app.world().contains_resource::<GpuReadback<Tag, T>>(),
367 "readback is already enabled for tag `{}`",
368 type_name::<Tag>()
369 );
370 let shared = Arc::new(ReadbackShared::default());
371 app.insert_resource(GpuReadback::<Tag, T> {
372 shared: shared.clone(),
373 latest: None,
374 _marker: PhantomData,
375 })
376 .add_systems(PreUpdate, deliver_slice::<Tag, T>);
377 shared
378}
379
380pub(crate) fn register_gpu<Tag, T>(app: &mut App, mode: ReadbackMode)
386where
387 Tag: Send + Sync + 'static,
388 T: ShaderSize + CreateFrom + Send + Sync + 'static,
389{
390 let Some(gpu_buffer) = app.world().get_resource::<GpuBuffer<Tag>>() else {
391 panic!(
392 "read_back_gpu_buffer::<{}, _>: call register_gpu_buffer for this tag first",
393 type_name::<Tag>()
394 );
395 };
396 let asset_id = gpu_buffer.handle().id();
397
398 add_readback_plugin(app);
399 let shared = insert_slice_readback::<Tag, T>(app);
400 add_render_job::<GpuKey<Tag>, _>(
401 app,
402 shared,
403 mode,
404 move |buffers: Res<RenderAssets<GpuShaderBuffer>>,
405 mut job: ResMut<ReadbackJob<GpuKey<Tag>>>,
406 render_device: Res<RenderDevice>,
407 mut copies: ResMut<ReadbackCopies>| {
408 if let Some(gpu_buffer) = buffers.get(asset_id) {
409 let size = gpu_buffer.buffer.size();
410 job.queue_copy(&gpu_buffer.buffer, size, &render_device, &mut copies);
411 }
412 },
413 );
414}
415
416pub(crate) fn register_array<Tag, T>(app: &mut App, mode: ReadbackMode)
418where
419 Tag: Send + Sync + 'static,
420 T: ShaderSize + CreateFrom + Send + Sync + 'static,
421{
422 assert!(
423 app.world().contains_resource::<ArrayBufferChanges<Tag>>(),
424 "read_back_array_buffer::<{}, _>: call register_array_buffer for this tag first",
425 type_name::<Tag>()
426 );
427
428 add_readback_plugin(app);
429 let shared = insert_slice_readback::<Tag, T>(app);
430 add_render_job::<ArrayKey<Tag>, _>(
431 app,
432 shared,
433 mode,
434 |state: Option<Res<ArrayBufferState<Tag>>>,
435 mut job: ResMut<ReadbackJob<ArrayKey<Tag>>>,
436 render_device: Res<RenderDevice>,
437 mut copies: ResMut<ReadbackCopies>| {
438 let Some(state) = state else { return };
439 if state.stride != array_stride::<T>() {
440 error_once!(
441 "read_back_array_buffer::<{}, {}>: element type doesn't match the one passed to register_array_buffer",
442 type_name::<Tag>(),
443 type_name::<T>()
444 );
445 return;
446 }
447 let size = state.buffer.size();
448 job.queue_copy(&state.buffer, size, &render_device, &mut copies);
449 },
450 );
451}
452
453pub(crate) fn register_storage<S>(app: &mut App, mode: ReadbackMode)
455where
456 S: ShaderType + WriteInto + Default + CreateFrom + Resource,
457{
458 let has_storage_slot = app
459 .get_sub_app(RenderApp)
460 .and_then(|render_app| render_app.world().get_resource::<CpuBuffer<S>>())
461 .is_some_and(|state| {
462 state
463 .slot_kinds()
464 .any(|kind| matches!(kind, AutoBufferKind::Storage { .. }))
465 });
466 assert!(
467 has_storage_slot,
468 "read_back_storage_buffer::<{}>: call register_storage_buffer for this type first",
469 type_name::<S>()
470 );
471 assert!(
472 !app.world().contains_resource::<StorageReadback<S>>(),
473 "read_back_storage_buffer::<{}>: readback is already enabled for this type",
474 type_name::<S>()
475 );
476
477 add_readback_plugin(app);
478 let shared = Arc::new(ReadbackShared::default());
479 app.insert_resource(StorageReadback::<S> {
480 shared: shared.clone(),
481 latest: None,
482 })
483 .add_systems(PreUpdate, deliver_storage::<S>);
484
485 add_render_job::<StorageKey<S>, _>(
486 app,
487 shared,
488 mode,
489 |state: Res<CpuBuffer<S>>,
490 mut job: ResMut<ReadbackJob<StorageKey<S>>>,
491 render_device: Res<RenderDevice>,
492 mut copies: ResMut<ReadbackCopies>| {
493 if let Some((buffer, size)) = state.readback_source() {
494 job.queue_copy(buffer, size, &render_device, &mut copies);
495 }
496 },
497 );
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn every_frame_copies_unless_in_flight() {
506 let requested = AtomicBool::new(false);
507 assert!(should_copy(ReadbackMode::EveryFrame, false, &requested));
508 assert!(!should_copy(ReadbackMode::EveryFrame, true, &requested));
509 }
510
511 #[test]
512 fn on_request_consumes_the_request() {
513 let requested = AtomicBool::new(true);
514 assert!(should_copy(ReadbackMode::OnRequest, false, &requested));
515 assert!(!requested.load(Ordering::Acquire));
516 assert!(!should_copy(ReadbackMode::OnRequest, false, &requested));
517 }
518
519 #[test]
520 fn on_request_stays_pending_while_in_flight() {
521 let requested = AtomicBool::new(true);
522 assert!(!should_copy(ReadbackMode::OnRequest, true, &requested));
523 assert!(requested.load(Ordering::Acquire));
524 }
525
526 #[test]
527 fn decodes_what_encase_wrote() {
528 #[derive(ShaderType, Debug, PartialEq)]
529 struct Sample {
530 a: Vec2,
531 b: f32,
532 }
533 let values = vec![
534 Sample {
535 a: Vec2::new(1.0, 2.0),
536 b: 3.0,
537 },
538 Sample {
539 a: Vec2::ZERO,
540 b: -1.0,
541 },
542 ];
543 let mut bytes = encase::StorageBuffer::new(Vec::new());
544 bytes.write(&values).unwrap();
545 assert_eq!(decode::<Vec<Sample>>(&bytes.into_inner()), Some(values));
546 }
547}