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