1use crate::{
2 extract_component::ExtractComponentPlugin,
3 render_asset::RenderAssets,
4 render_resource::{
5 Buffer, BufferUsages, CommandEncoder, Extent3d, TexelCopyBufferLayout, Texture,
6 TextureFormat,
7 },
8 renderer::RenderDevice,
9 storage::{GpuShaderBuffer, ShaderBuffer},
10 sync_world::MainEntity,
11 texture::GpuImage,
12 ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems,
13};
14use async_channel::{Receiver, Sender};
15use bevy_app::{App, First, Plugin};
16use bevy_asset::Handle;
17use bevy_derive::{Deref, DerefMut};
18use bevy_ecs::{
19 bundle::Bundle,
20 change_detection::ResMut,
21 entity::Entity,
22 event::EntityEvent,
23 prelude::{Component, Resource, World},
24 query::With,
25 system::{Commands, Query, Res},
26};
27use bevy_ecs::{schedule::IntoScheduleConfigs, template::FromTemplate};
28use bevy_extract_macros::ExtractComponent;
29use bevy_image::{Image, TextureFormatPixelInfo};
30use bevy_log::{debug, warn};
31use bevy_platform::collections::HashMap;
32use bevy_reflect::Reflect;
33use encase::internal::ReadFrom;
34use encase::private::Reader;
35use encase::ShaderType;
36
37pub struct GpuReadbackPlugin {
39 max_unused_frames: usize,
42}
43
44impl Default for GpuReadbackPlugin {
45 fn default() -> Self {
46 Self {
47 max_unused_frames: 10,
48 }
49 }
50}
51
52impl Plugin for GpuReadbackPlugin {
53 fn build(&self, app: &mut App) {
54 app.add_plugins(ExtractComponentPlugin::<Readback>::default())
55 .register_type::<ReadbackOnce>()
56 .add_systems(
57 First,
58 cleanup_readback_once.after(bevy_ecs::message::MessageUpdateSystems),
59 );
60
61 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
62 render_app
63 .init_resource::<GpuReadbackBufferPool>()
64 .init_resource::<GpuReadbacks>()
65 .insert_resource(GpuReadbackMaxUnusedFrames(self.max_unused_frames))
66 .add_systems(ExtractSchedule, sync_readbacks.ambiguous_with_all())
67 .add_systems(
68 Render,
69 (
70 prepare_buffers.in_set(RenderSystems::PrepareResources),
71 map_buffers.in_set(RenderSystems::Cleanup),
73 ),
74 );
75 }
76 }
77}
78
79#[derive(Component, ExtractComponent, Clone, Debug, FromTemplate)]
84#[extract_app(RenderApp)]
85pub enum Readback {
86 #[default]
87 Texture(Handle<Image>),
88 Buffer {
89 buffer: Handle<ShaderBuffer>,
90 start_offset_and_size: Option<(u64, u64)>,
91 },
92}
93
94impl Readback {
95 pub fn texture(image: Handle<Image>) -> Self {
97 Self::Texture(image)
98 }
99
100 pub fn buffer(buffer: Handle<ShaderBuffer>) -> Self {
102 Self::Buffer {
103 buffer,
104 start_offset_and_size: None,
105 }
106 }
107
108 pub fn buffer_range(buffer: Handle<ShaderBuffer>, start_offset: u64, size: u64) -> Self {
111 Self::Buffer {
112 buffer,
113 start_offset_and_size: Some((start_offset, size)),
114 }
115 }
116}
117
118#[derive(Component, Clone, Debug, Default, Reflect)]
125pub struct ReadbackOnce;
126
127impl ReadbackOnce {
128 pub fn texture(image: Handle<Image>) -> impl Bundle {
130 (Readback::texture(image), Self)
131 }
132
133 pub fn buffer(buffer: Handle<ShaderBuffer>) -> impl Bundle {
135 (Readback::buffer(buffer), Self)
136 }
137
138 pub fn buffer_range(buffer: Handle<ShaderBuffer>, start_offset: u64, size: u64) -> impl Bundle {
141 (Readback::buffer_range(buffer, start_offset, size), Self)
142 }
143}
144
145fn cleanup_readback_once(mut commands: Commands, query: Query<Entity, With<ReadbackOnce>>) {
146 for entity in &query {
147 commands.entity(entity).remove::<(Readback, ReadbackOnce)>();
148 }
149}
150
151#[derive(EntityEvent, Deref, DerefMut, Reflect, Debug)]
156#[reflect(Debug)]
157pub struct ReadbackComplete {
158 pub entity: Entity,
159 #[deref]
160 pub data: Vec<u8>,
161}
162
163impl ReadbackComplete {
164 pub fn to_shader_type<T: ShaderType + ReadFrom + Default>(&self) -> T {
166 let mut val = T::default();
167 let mut reader = Reader::new::<T>(&self.data, 0).expect("Failed to create Reader");
168 T::read_from(&mut val, &mut reader);
169 val
170 }
171}
172
173#[derive(Resource)]
174struct GpuReadbackMaxUnusedFrames(usize);
175
176struct GpuReadbackBuffer {
177 buffer: Buffer,
178 taken: bool,
179 frames_unused: usize,
180}
181
182#[derive(Resource, Default)]
183struct GpuReadbackBufferPool {
184 buffers: HashMap<u64, Vec<GpuReadbackBuffer>>,
190}
191
192impl GpuReadbackBufferPool {
193 fn get(&mut self, render_device: &RenderDevice, size: u64) -> Buffer {
194 let buffers = self.buffers.entry(size).or_default();
195
196 if let Some(buf) = buffers.iter_mut().find(|x| !x.taken) {
198 buf.taken = true;
199 buf.frames_unused = 0;
200 return buf.buffer.clone();
201 }
202
203 let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
204 label: Some("Readback Buffer"),
205 size,
206 usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
207 mapped_at_creation: false,
208 });
209 buffers.push(GpuReadbackBuffer {
210 buffer: buffer.clone(),
211 taken: true,
212 frames_unused: 0,
213 });
214 buffer
215 }
216
217 fn return_buffer(&mut self, buffer: &Buffer) {
219 let size = buffer.size();
220 let buffers = self
221 .buffers
222 .get_mut(&size)
223 .expect("Returned buffer of untracked size");
224 if let Some(buf) = buffers.iter_mut().find(|x| x.buffer.id() == buffer.id()) {
225 buf.taken = false;
226 } else {
227 warn!("Returned buffer that was not allocated");
228 }
229 }
230
231 fn update(&mut self, max_unused_frames: usize) {
232 for (_, buffers) in &mut self.buffers {
233 for buf in &mut *buffers {
235 if !buf.taken {
236 buf.frames_unused += 1;
237 }
238 }
239
240 buffers.retain(|x| x.frames_unused < max_unused_frames);
242 }
243
244 self.buffers.retain(|_, buffers| !buffers.is_empty());
246 }
247}
248
249enum ReadbackSource {
250 Texture {
251 texture: Texture,
252 layout: TexelCopyBufferLayout,
253 size: Extent3d,
254 },
255 Buffer {
256 buffer: Buffer,
257 start_offset_and_size: Option<(u64, u64)>,
258 },
259}
260
261#[derive(Resource, Default)]
262struct GpuReadbacks {
263 requested: Vec<GpuReadback>,
264 mapped: Vec<GpuReadback>,
265}
266
267struct GpuReadback {
268 pub entity: Entity,
269 pub src: ReadbackSource,
270 pub buffer: Buffer,
271 pub rx: Receiver<(Entity, Buffer, Vec<u8>)>,
272 pub tx: Sender<(Entity, Buffer, Vec<u8>)>,
273}
274
275fn sync_readbacks(
276 mut main_world: ResMut<MainWorld>,
277 mut buffer_pool: ResMut<GpuReadbackBufferPool>,
278 mut readbacks: ResMut<GpuReadbacks>,
279 max_unused_frames: Res<GpuReadbackMaxUnusedFrames>,
280) {
281 readbacks.mapped.retain(|readback| {
282 if let Ok((entity, buffer, data)) = readback.rx.try_recv() {
283 main_world.trigger(ReadbackComplete { data, entity });
284 buffer_pool.return_buffer(&buffer);
285 false
286 } else {
287 true
288 }
289 });
290
291 buffer_pool.update(max_unused_frames.0);
292}
293
294fn prepare_buffers(
295 render_device: Res<RenderDevice>,
296 mut readbacks: ResMut<GpuReadbacks>,
297 mut buffer_pool: ResMut<GpuReadbackBufferPool>,
298 gpu_images: Res<RenderAssets<GpuImage>>,
299 ssbos: Res<RenderAssets<GpuShaderBuffer>>,
300 handles: Query<(&MainEntity, &Readback)>,
301) {
302 for (entity, readback) in handles.iter() {
303 match readback {
304 Readback::Texture(image) => {
305 if let Some(gpu_image) = gpu_images.get(image)
306 && let Ok(pixel_size) = gpu_image.texture_descriptor.format.pixel_size()
307 {
308 let layout = layout_data(
309 gpu_image.texture_descriptor.size,
310 gpu_image.texture_descriptor.format,
311 );
312 let buffer = buffer_pool.get(
313 &render_device,
314 get_aligned_size(gpu_image.texture_descriptor.size, pixel_size as u32)
315 as u64,
316 );
317 let (tx, rx) = async_channel::bounded(1);
318 readbacks.requested.push(GpuReadback {
319 entity: entity.id(),
320 src: ReadbackSource::Texture {
321 texture: gpu_image.texture.clone(),
322 layout,
323 size: gpu_image.texture_descriptor.size,
324 },
325 buffer,
326 rx,
327 tx,
328 });
329 }
330 }
331 Readback::Buffer {
332 buffer,
333 start_offset_and_size,
334 } => {
335 if let Some(ssbo) = ssbos.get(buffer) {
336 let full_size = ssbo.buffer.size();
337 let size = start_offset_and_size
338 .map(|(start, size)| {
339 let end = start + size;
340 if end > full_size {
341 panic!(
342 "Tried to read past the end of the buffer (start: {start}, \
343 size: {size}, buffer size: {full_size})."
344 );
345 }
346 size
347 })
348 .unwrap_or(full_size);
349 let buffer = buffer_pool.get(&render_device, size);
350 let (tx, rx) = async_channel::bounded(1);
351 readbacks.requested.push(GpuReadback {
352 entity: entity.id(),
353 src: ReadbackSource::Buffer {
354 start_offset_and_size: *start_offset_and_size,
355 buffer: ssbo.buffer.clone(),
356 },
357 buffer,
358 rx,
359 tx,
360 });
361 }
362 }
363 }
364 }
365}
366
367pub(crate) fn submit_readback_commands(world: &World, command_encoder: &mut CommandEncoder) {
368 let readbacks = world.resource::<GpuReadbacks>();
369 for readback in &readbacks.requested {
370 match &readback.src {
371 ReadbackSource::Texture {
372 texture,
373 layout,
374 size,
375 } => {
376 command_encoder.copy_texture_to_buffer(
377 texture.as_image_copy(),
378 wgpu::TexelCopyBufferInfo {
379 buffer: &readback.buffer,
380 layout: *layout,
381 },
382 *size,
383 );
384 }
385 ReadbackSource::Buffer {
386 buffer,
387 start_offset_and_size,
388 } => {
389 let (src_start, size) = start_offset_and_size.unwrap_or((0, buffer.size()));
390 command_encoder.copy_buffer_to_buffer(buffer, src_start, &readback.buffer, 0, size);
391 }
392 }
393 }
394}
395
396#[expect(
398 clippy::drain_collect,
399 reason = "draining preserves the capacity of `requested`, which is refilled every frame"
400)]
401fn map_buffers(mut readbacks: ResMut<GpuReadbacks>) {
402 let requested = readbacks.requested.drain(..).collect::<Vec<GpuReadback>>();
403 for readback in requested {
404 let slice = readback.buffer.slice(..);
405 let entity = readback.entity;
406 let buffer = readback.buffer.clone();
407 let tx = readback.tx.clone();
408 slice.map_async(wgpu::MapMode::Read, move |res| {
409 res.expect("Failed to map buffer");
410 let buffer_slice = buffer.slice(..);
411 let data = buffer_slice.get_mapped_range().unwrap();
412 let result = Vec::from(&*data);
413 drop(data);
414 buffer.unmap();
415 if let Err(e) = tx.try_send((entity, buffer, result)) {
416 debug!("Failed to send readback result: {}", e);
417 }
418 });
419 readbacks.mapped.push(readback);
420 }
421}
422
423pub(crate) const fn align_byte_size(value: u32) -> u32 {
427 RenderDevice::align_copy_bytes_per_row(value as usize) as u32
428}
429
430pub(crate) const fn get_aligned_size(extent: Extent3d, pixel_size: u32) -> u32 {
432 extent.height * align_byte_size(extent.width * pixel_size) * extent.depth_or_array_layers
433}
434
435pub(crate) fn layout_data(extent: Extent3d, format: TextureFormat) -> TexelCopyBufferLayout {
437 TexelCopyBufferLayout {
438 bytes_per_row: if extent.height > 1 || extent.depth_or_array_layers > 1 {
439 if let Ok(pixel_size) = format.pixel_size() {
440 Some(get_aligned_size(
442 Extent3d {
443 width: extent.width,
444 height: 1,
445 depth_or_array_layers: 1,
446 },
447 pixel_size as u32,
448 ))
449 } else {
450 None
451 }
452 } else {
453 None
454 },
455 rows_per_image: if extent.depth_or_array_layers > 1 {
456 let (_, block_dimension_y) = format.block_dimensions();
457 Some(extent.height / block_dimension_y)
458 } else {
459 None
460 },
461 offset: 0,
462 }
463}