Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_render/src/gpu_readback.rs
9320 views
1
use 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
};
14
use async_channel::{Receiver, Sender};
15
use bevy_app::{App, Plugin};
16
use bevy_asset::Handle;
17
use bevy_derive::{Deref, DerefMut};
18
use bevy_ecs::schedule::IntoScheduleConfigs;
19
use bevy_ecs::{
20
change_detection::ResMut,
21
entity::Entity,
22
event::EntityEvent,
23
prelude::{Component, Resource, World},
24
system::{Query, Res},
25
};
26
use bevy_image::{Image, TextureFormatPixelInfo};
27
use bevy_log::warn;
28
use bevy_platform::collections::HashMap;
29
use bevy_reflect::Reflect;
30
use bevy_render_macros::ExtractComponent;
31
use encase::internal::ReadFrom;
32
use encase::private::Reader;
33
use encase::ShaderType;
34
35
/// A plugin that enables reading back gpu buffers and textures to the cpu.
36
pub struct GpuReadbackPlugin {
37
/// Describes the number of frames a buffer can be unused before it is removed from the pool in
38
/// order to avoid unnecessary reallocations.
39
max_unused_frames: usize,
40
}
41
42
impl Default for GpuReadbackPlugin {
43
fn default() -> Self {
44
Self {
45
max_unused_frames: 10,
46
}
47
}
48
}
49
50
impl Plugin for GpuReadbackPlugin {
51
fn build(&self, app: &mut App) {
52
app.add_plugins(ExtractComponentPlugin::<Readback>::default());
53
54
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
55
render_app
56
.init_resource::<GpuReadbackBufferPool>()
57
.init_resource::<GpuReadbacks>()
58
.insert_resource(GpuReadbackMaxUnusedFrames(self.max_unused_frames))
59
.add_systems(ExtractSchedule, sync_readbacks.ambiguous_with_all())
60
.add_systems(
61
Render,
62
(
63
prepare_buffers.in_set(RenderSystems::PrepareResources),
64
// TODO: this should be in the graph somehow
65
map_buffers.in_set(RenderSystems::Cleanup),
66
),
67
);
68
}
69
}
70
}
71
72
/// A component that registers the wrapped handle for gpu readback, either a texture or a buffer.
73
///
74
/// Data is read asynchronously and will be triggered on the entity via the [`ReadbackComplete`] event
75
/// when complete. If this component is not removed, the readback will be attempted every frame
76
#[derive(Component, ExtractComponent, Clone, Debug)]
77
pub enum Readback {
78
Texture(Handle<Image>),
79
Buffer {
80
buffer: Handle<ShaderBuffer>,
81
start_offset_and_size: Option<(u64, u64)>,
82
},
83
}
84
85
impl Readback {
86
/// Create a readback component for a texture using the given handle.
87
pub fn texture(image: Handle<Image>) -> Self {
88
Self::Texture(image)
89
}
90
91
/// Create a readback component for a full buffer using the given handle.
92
pub fn buffer(buffer: Handle<ShaderBuffer>) -> Self {
93
Self::Buffer {
94
buffer,
95
start_offset_and_size: None,
96
}
97
}
98
99
/// Create a readback component for a buffer range using the given handle, a start offset in bytes
100
/// and a number of bytes to read.
101
pub fn buffer_range(buffer: Handle<ShaderBuffer>, start_offset: u64, size: u64) -> Self {
102
Self::Buffer {
103
buffer,
104
start_offset_and_size: Some((start_offset, size)),
105
}
106
}
107
}
108
109
/// An event that is triggered when a gpu readback is complete.
110
///
111
/// The event contains the data as a `Vec<u8>`, which can be interpreted as the raw bytes of the
112
/// requested buffer or texture.
113
#[derive(EntityEvent, Deref, DerefMut, Reflect, Debug)]
114
#[reflect(Debug)]
115
pub struct ReadbackComplete {
116
pub entity: Entity,
117
#[deref]
118
pub data: Vec<u8>,
119
}
120
121
impl ReadbackComplete {
122
/// Convert the raw bytes of the event to a shader type.
123
pub fn to_shader_type<T: ShaderType + ReadFrom + Default>(&self) -> T {
124
let mut val = T::default();
125
let mut reader = Reader::new::<T>(&self.data, 0).expect("Failed to create Reader");
126
T::read_from(&mut val, &mut reader);
127
val
128
}
129
}
130
131
#[derive(Resource)]
132
struct GpuReadbackMaxUnusedFrames(usize);
133
134
struct GpuReadbackBuffer {
135
buffer: Buffer,
136
taken: bool,
137
frames_unused: usize,
138
}
139
140
#[derive(Resource, Default)]
141
struct GpuReadbackBufferPool {
142
// Map of buffer size to list of buffers, with a flag for whether the buffer is taken and how
143
// many frames it has been unused for.
144
// TODO: We could ideally write all readback data to one big buffer per frame, the assumption
145
// here is that very few entities well actually be read back at once, and their size is
146
// unlikely to change.
147
buffers: HashMap<u64, Vec<GpuReadbackBuffer>>,
148
}
149
150
impl GpuReadbackBufferPool {
151
fn get(&mut self, render_device: &RenderDevice, size: u64) -> Buffer {
152
let buffers = self.buffers.entry(size).or_default();
153
154
// find an untaken buffer for this size
155
if let Some(buf) = buffers.iter_mut().find(|x| !x.taken) {
156
buf.taken = true;
157
buf.frames_unused = 0;
158
return buf.buffer.clone();
159
}
160
161
let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
162
label: Some("Readback Buffer"),
163
size,
164
usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
165
mapped_at_creation: false,
166
});
167
buffers.push(GpuReadbackBuffer {
168
buffer: buffer.clone(),
169
taken: true,
170
frames_unused: 0,
171
});
172
buffer
173
}
174
175
// Returns the buffer to the pool so it can be used in a future frame
176
fn return_buffer(&mut self, buffer: &Buffer) {
177
let size = buffer.size();
178
let buffers = self
179
.buffers
180
.get_mut(&size)
181
.expect("Returned buffer of untracked size");
182
if let Some(buf) = buffers.iter_mut().find(|x| x.buffer.id() == buffer.id()) {
183
buf.taken = false;
184
} else {
185
warn!("Returned buffer that was not allocated");
186
}
187
}
188
189
fn update(&mut self, max_unused_frames: usize) {
190
for (_, buffers) in &mut self.buffers {
191
// Tick all the buffers
192
for buf in &mut *buffers {
193
if !buf.taken {
194
buf.frames_unused += 1;
195
}
196
}
197
198
// Remove buffers that haven't been used for MAX_UNUSED_FRAMES
199
buffers.retain(|x| x.frames_unused < max_unused_frames);
200
}
201
202
// Remove empty buffer sizes
203
self.buffers.retain(|_, buffers| !buffers.is_empty());
204
}
205
}
206
207
enum ReadbackSource {
208
Texture {
209
texture: Texture,
210
layout: TexelCopyBufferLayout,
211
size: Extent3d,
212
},
213
Buffer {
214
buffer: Buffer,
215
start_offset_and_size: Option<(u64, u64)>,
216
},
217
}
218
219
#[derive(Resource, Default)]
220
struct GpuReadbacks {
221
requested: Vec<GpuReadback>,
222
mapped: Vec<GpuReadback>,
223
}
224
225
struct GpuReadback {
226
pub entity: Entity,
227
pub src: ReadbackSource,
228
pub buffer: Buffer,
229
pub rx: Receiver<(Entity, Buffer, Vec<u8>)>,
230
pub tx: Sender<(Entity, Buffer, Vec<u8>)>,
231
}
232
233
fn sync_readbacks(
234
mut main_world: ResMut<MainWorld>,
235
mut buffer_pool: ResMut<GpuReadbackBufferPool>,
236
mut readbacks: ResMut<GpuReadbacks>,
237
max_unused_frames: Res<GpuReadbackMaxUnusedFrames>,
238
) {
239
readbacks.mapped.retain(|readback| {
240
if let Ok((entity, buffer, data)) = readback.rx.try_recv() {
241
main_world.trigger(ReadbackComplete { data, entity });
242
buffer_pool.return_buffer(&buffer);
243
false
244
} else {
245
true
246
}
247
});
248
249
buffer_pool.update(max_unused_frames.0);
250
}
251
252
fn prepare_buffers(
253
render_device: Res<RenderDevice>,
254
mut readbacks: ResMut<GpuReadbacks>,
255
mut buffer_pool: ResMut<GpuReadbackBufferPool>,
256
gpu_images: Res<RenderAssets<GpuImage>>,
257
ssbos: Res<RenderAssets<GpuShaderBuffer>>,
258
handles: Query<(&MainEntity, &Readback)>,
259
) {
260
for (entity, readback) in handles.iter() {
261
match readback {
262
Readback::Texture(image) => {
263
if let Some(gpu_image) = gpu_images.get(image)
264
&& let Ok(pixel_size) = gpu_image.texture_descriptor.format.pixel_size()
265
{
266
let layout = layout_data(
267
gpu_image.texture_descriptor.size,
268
gpu_image.texture_descriptor.format,
269
);
270
let buffer = buffer_pool.get(
271
&render_device,
272
get_aligned_size(gpu_image.texture_descriptor.size, pixel_size as u32)
273
as u64,
274
);
275
let (tx, rx) = async_channel::bounded(1);
276
readbacks.requested.push(GpuReadback {
277
entity: entity.id(),
278
src: ReadbackSource::Texture {
279
texture: gpu_image.texture.clone(),
280
layout,
281
size: gpu_image.texture_descriptor.size,
282
},
283
buffer,
284
rx,
285
tx,
286
});
287
}
288
}
289
Readback::Buffer {
290
buffer,
291
start_offset_and_size,
292
} => {
293
if let Some(ssbo) = ssbos.get(buffer) {
294
let full_size = ssbo.buffer.size();
295
let size = start_offset_and_size
296
.map(|(start, size)| {
297
let end = start + size;
298
if end > full_size {
299
panic!(
300
"Tried to read past the end of the buffer (start: {start}, \
301
size: {size}, buffer size: {full_size})."
302
);
303
}
304
size
305
})
306
.unwrap_or(full_size);
307
let buffer = buffer_pool.get(&render_device, size);
308
let (tx, rx) = async_channel::bounded(1);
309
readbacks.requested.push(GpuReadback {
310
entity: entity.id(),
311
src: ReadbackSource::Buffer {
312
start_offset_and_size: *start_offset_and_size,
313
buffer: ssbo.buffer.clone(),
314
},
315
buffer,
316
rx,
317
tx,
318
});
319
}
320
}
321
}
322
}
323
}
324
325
pub(crate) fn submit_readback_commands(world: &World, command_encoder: &mut CommandEncoder) {
326
let readbacks = world.resource::<GpuReadbacks>();
327
for readback in &readbacks.requested {
328
match &readback.src {
329
ReadbackSource::Texture {
330
texture,
331
layout,
332
size,
333
} => {
334
command_encoder.copy_texture_to_buffer(
335
texture.as_image_copy(),
336
wgpu::TexelCopyBufferInfo {
337
buffer: &readback.buffer,
338
layout: *layout,
339
},
340
*size,
341
);
342
}
343
ReadbackSource::Buffer {
344
buffer,
345
start_offset_and_size,
346
} => {
347
let (src_start, size) = start_offset_and_size.unwrap_or((0, buffer.size()));
348
command_encoder.copy_buffer_to_buffer(buffer, src_start, &readback.buffer, 0, size);
349
}
350
}
351
}
352
}
353
354
/// Move requested readbacks to mapped readbacks after commands have been submitted in render system
355
fn map_buffers(mut readbacks: ResMut<GpuReadbacks>) {
356
let requested = readbacks.requested.drain(..).collect::<Vec<GpuReadback>>();
357
for readback in requested {
358
let slice = readback.buffer.slice(..);
359
let entity = readback.entity;
360
let buffer = readback.buffer.clone();
361
let tx = readback.tx.clone();
362
slice.map_async(wgpu::MapMode::Read, move |res| {
363
res.expect("Failed to map buffer");
364
let buffer_slice = buffer.slice(..);
365
let data = buffer_slice.get_mapped_range();
366
let result = Vec::from(&*data);
367
drop(data);
368
buffer.unmap();
369
if let Err(e) = tx.try_send((entity, buffer, result)) {
370
warn!("Failed to send readback result: {}", e);
371
}
372
});
373
readbacks.mapped.push(readback);
374
}
375
}
376
377
// Utils
378
379
/// Round up a given value to be a multiple of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`].
380
pub(crate) const fn align_byte_size(value: u32) -> u32 {
381
RenderDevice::align_copy_bytes_per_row(value as usize) as u32
382
}
383
384
/// Get the size of a image when the size of each row has been rounded up to [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`].
385
pub(crate) const fn get_aligned_size(extent: Extent3d, pixel_size: u32) -> u32 {
386
extent.height * align_byte_size(extent.width * pixel_size) * extent.depth_or_array_layers
387
}
388
389
/// Get a [`TexelCopyBufferLayout`] aligned such that the image can be copied into a buffer.
390
pub(crate) fn layout_data(extent: Extent3d, format: TextureFormat) -> TexelCopyBufferLayout {
391
TexelCopyBufferLayout {
392
bytes_per_row: if extent.height > 1 || extent.depth_or_array_layers > 1 {
393
if let Ok(pixel_size) = format.pixel_size() {
394
// 1 = 1 row
395
Some(get_aligned_size(
396
Extent3d {
397
width: extent.width,
398
height: 1,
399
depth_or_array_layers: 1,
400
},
401
pixel_size as u32,
402
))
403
} else {
404
None
405
}
406
} else {
407
None
408
},
409
rows_per_image: if extent.depth_or_array_layers > 1 {
410
let (_, block_dimension_y) = format.block_dimensions();
411
Some(extent.height / block_dimension_y)
412
} else {
413
None
414
},
415
offset: 0,
416
}
417
}
418
419