Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_render/src/view/window/screenshot.rs
9371 views
1
use super::ExtractedWindows;
2
use crate::{
3
gpu_readback,
4
render_asset::RenderAssets,
5
render_resource::{
6
BindGroup, BindGroupEntries, Buffer, BufferUsages, PipelineCache,
7
SpecializedRenderPipeline, SpecializedRenderPipelines, Texture, TextureUsages, TextureView,
8
},
9
renderer::RenderDevice,
10
texture::{GpuImage, ManualTextureViews, OutputColorAttachment},
11
view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces},
12
ExtractSchedule, MainWorld, Render, RenderApp, RenderStartup, RenderSystems,
13
};
14
use alloc::{borrow::Cow, sync::Arc};
15
use bevy_app::{First, Plugin, Update};
16
use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle, RenderAssetUsages};
17
use bevy_camera::{ManualTextureViewHandle, NormalizedRenderTarget, RenderTarget};
18
use bevy_derive::{Deref, DerefMut};
19
use bevy_ecs::{
20
entity::EntityHashMap, message::message_update_system, prelude::*, system::SystemState,
21
};
22
use bevy_image::{Image, TextureFormatPixelInfo, ToExtents};
23
use bevy_log::{error, info, warn};
24
use bevy_material::{
25
bind_group_layout_entries::{binding_types::texture_2d, BindGroupLayoutEntries},
26
descriptor::{
27
BindGroupLayoutDescriptor, CachedRenderPipelineId, FragmentState, RenderPipelineDescriptor,
28
VertexState,
29
},
30
};
31
use bevy_platform::collections::HashSet;
32
use bevy_reflect::Reflect;
33
use bevy_shader::Shader;
34
use bevy_tasks::AsyncComputeTaskPool;
35
use bevy_utils::default;
36
use bevy_window::{PrimaryWindow, WindowRef};
37
use core::ops::Deref;
38
use std::{
39
path::Path,
40
sync::{
41
mpsc::{Receiver, Sender},
42
Mutex,
43
},
44
};
45
use wgpu::{CommandEncoder, Extent3d, TextureFormat};
46
47
#[derive(EntityEvent, Reflect, Deref, DerefMut, Debug)]
48
#[reflect(Debug)]
49
pub struct ScreenshotCaptured {
50
pub entity: Entity,
51
#[deref]
52
pub image: Image,
53
}
54
55
/// A component that signals to the renderer to capture a screenshot this frame.
56
///
57
/// This component should be spawned on a new entity with an observer that will trigger
58
/// with [`ScreenshotCaptured`] when the screenshot is ready.
59
///
60
/// Screenshots are captured asynchronously and may not be available immediately after the frame
61
/// that the component is spawned on. The observer should be used to handle the screenshot when it
62
/// is ready.
63
///
64
/// Note that the screenshot entity will be despawned after the screenshot is captured and the
65
/// observer is triggered.
66
///
67
/// # Usage
68
///
69
/// ```
70
/// # use bevy_ecs::prelude::*;
71
/// # use bevy_render::view::screenshot::{save_to_disk, Screenshot};
72
///
73
/// fn take_screenshot(mut commands: Commands) {
74
/// commands.spawn(Screenshot::primary_window())
75
/// .observe(save_to_disk("screenshot.png"));
76
/// }
77
/// ```
78
#[derive(Component, Deref, DerefMut, Reflect, Debug)]
79
#[reflect(Component, Debug)]
80
pub struct Screenshot(pub RenderTarget);
81
82
/// A marker component that indicates that a screenshot is currently being captured.
83
#[derive(Component, Default)]
84
pub struct Capturing;
85
86
/// A marker component that indicates that a screenshot has been captured, the image is ready, and
87
/// the screenshot entity can be despawned.
88
#[derive(Component, Default)]
89
pub struct Captured;
90
91
impl Screenshot {
92
/// Capture a screenshot of the provided window entity.
93
pub fn window(window: Entity) -> Self {
94
Self(RenderTarget::Window(WindowRef::Entity(window)))
95
}
96
97
/// Capture a screenshot of the primary window, if one exists.
98
pub fn primary_window() -> Self {
99
Self(RenderTarget::Window(WindowRef::Primary))
100
}
101
102
/// Capture a screenshot of the provided render target image.
103
pub fn image(image: Handle<Image>) -> Self {
104
Self(RenderTarget::Image(image.into()))
105
}
106
107
/// Capture a screenshot of the provided manual texture view.
108
pub fn texture_view(texture_view: ManualTextureViewHandle) -> Self {
109
Self(RenderTarget::TextureView(texture_view))
110
}
111
}
112
113
struct ScreenshotPreparedState {
114
pub texture: Texture,
115
pub buffer: Buffer,
116
pub bind_group: BindGroup,
117
pub pipeline_id: CachedRenderPipelineId,
118
pub size: Extent3d,
119
}
120
121
#[derive(Resource, Deref, DerefMut)]
122
pub struct CapturedScreenshots(pub Arc<Mutex<Receiver<(Entity, Image)>>>);
123
124
#[derive(Resource, Deref, DerefMut, Default)]
125
struct RenderScreenshotTargets(EntityHashMap<NormalizedRenderTarget>);
126
127
#[derive(Resource, Deref, DerefMut, Default)]
128
struct RenderScreenshotsPrepared(EntityHashMap<ScreenshotPreparedState>);
129
130
#[derive(Resource, Deref, DerefMut)]
131
struct RenderScreenshotsSender(Sender<(Entity, Image)>);
132
133
/// Saves the captured screenshot to disk at the provided path.
134
pub fn save_to_disk(path: impl AsRef<Path>) -> impl FnMut(On<ScreenshotCaptured>) {
135
let path = path.as_ref().to_owned();
136
move |screenshot_captured| {
137
let img = screenshot_captured.image.clone();
138
match img.try_into_dynamic() {
139
Ok(dyn_img) => match image::ImageFormat::from_path(&path) {
140
Ok(format) => {
141
// discard the alpha channel which stores brightness values when HDR is enabled to make sure
142
// the screenshot looks right
143
let img = dyn_img.to_rgb8();
144
#[cfg(not(target_arch = "wasm32"))]
145
match img.save_with_format(&path, format) {
146
Ok(_) => info!("Screenshot saved to {}", path.display()),
147
Err(e) => error!("Cannot save screenshot, IO error: {e}"),
148
}
149
150
#[cfg(target_arch = "wasm32")]
151
{
152
let save_screenshot = || {
153
use image::EncodableLayout;
154
use wasm_bindgen::{JsCast, JsValue};
155
156
let mut image_buffer = std::io::Cursor::new(Vec::new());
157
img.write_to(&mut image_buffer, format)
158
.map_err(|e| JsValue::from_str(&format!("{e}")))?;
159
160
let parts = js_sys::Array::of1(
161
&js_sys::Uint8Array::new_from_slice(
162
image_buffer.into_inner().as_bytes(),
163
)
164
.into(),
165
);
166
let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)?;
167
let url = web_sys::Url::create_object_url_with_blob(&blob)?;
168
let window = web_sys::window().unwrap();
169
let document = window.document().unwrap();
170
let link = document.create_element("a")?;
171
link.set_attribute("href", &url)?;
172
link.set_attribute(
173
"download",
174
path.file_name()
175
.and_then(|filename| filename.to_str())
176
.ok_or_else(|| JsValue::from_str("Invalid filename"))?,
177
)?;
178
let html_element = link.dyn_into::<web_sys::HtmlElement>()?;
179
html_element.click();
180
web_sys::Url::revoke_object_url(&url)?;
181
Ok::<(), JsValue>(())
182
};
183
184
match (save_screenshot)() {
185
Ok(_) => info!("Screenshot saved to {}", path.display()),
186
Err(e) => error!("Cannot save screenshot, error: {e:?}"),
187
};
188
}
189
}
190
Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"),
191
},
192
Err(e) => error!("Cannot save screenshot, screen format cannot be understood: {e}"),
193
}
194
}
195
}
196
197
fn clear_screenshots(mut commands: Commands, screenshots: Query<Entity, With<Captured>>) {
198
for entity in screenshots.iter() {
199
commands.entity(entity).despawn();
200
}
201
}
202
203
pub fn trigger_screenshots(
204
mut commands: Commands,
205
captured_screenshots: ResMut<CapturedScreenshots>,
206
) {
207
let captured_screenshots = captured_screenshots.lock().unwrap();
208
while let Ok((entity, image)) = captured_screenshots.try_recv() {
209
commands.entity(entity).insert(Captured);
210
commands.trigger(ScreenshotCaptured { image, entity });
211
}
212
}
213
214
fn extract_screenshots(
215
mut targets: ResMut<RenderScreenshotTargets>,
216
mut main_world: ResMut<MainWorld>,
217
mut system_state: Local<
218
Option<
219
SystemState<(
220
Commands,
221
Query<Entity, With<PrimaryWindow>>,
222
Query<(Entity, &Screenshot), Without<Capturing>>,
223
)>,
224
>,
225
>,
226
mut seen_targets: Local<HashSet<NormalizedRenderTarget>>,
227
) {
228
if system_state.is_none() {
229
*system_state = Some(SystemState::new(&mut main_world));
230
}
231
let system_state = system_state.as_mut().unwrap();
232
let (mut commands, primary_window, screenshots) = system_state.get_mut(&mut main_world);
233
234
targets.clear();
235
seen_targets.clear();
236
237
let primary_window = primary_window.iter().next();
238
239
for (entity, screenshot) in screenshots.iter() {
240
let render_target = screenshot.0.clone();
241
let Some(render_target) = render_target.normalize(primary_window) else {
242
warn!(
243
"Unknown render target for screenshot, skipping: {:?}",
244
render_target
245
);
246
continue;
247
};
248
if seen_targets.contains(&render_target) {
249
warn!(
250
"Duplicate render target for screenshot, skipping entity {}: {:?}",
251
entity, render_target
252
);
253
// If we don't despawn the entity here, it will be captured again in the next frame
254
commands.entity(entity).despawn();
255
continue;
256
}
257
seen_targets.insert(render_target.clone());
258
targets.insert(entity, render_target);
259
commands.entity(entity).insert(Capturing);
260
}
261
262
system_state.apply(&mut main_world);
263
}
264
265
fn prepare_screenshots(
266
targets: Res<RenderScreenshotTargets>,
267
mut prepared: ResMut<RenderScreenshotsPrepared>,
268
window_surfaces: Res<WindowSurfaces>,
269
render_device: Res<RenderDevice>,
270
screenshot_pipeline: Res<ScreenshotToScreenPipeline>,
271
pipeline_cache: Res<PipelineCache>,
272
mut pipelines: ResMut<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>,
273
images: Res<RenderAssets<GpuImage>>,
274
manual_texture_views: Res<ManualTextureViews>,
275
mut view_target_attachments: ResMut<ViewTargetAttachments>,
276
) {
277
prepared.clear();
278
for (entity, target) in targets.iter() {
279
match target {
280
NormalizedRenderTarget::Window(window) => {
281
let window = window.entity();
282
let Some(surface_data) = window_surfaces.surfaces.get(&window) else {
283
warn!("Unknown window for screenshot, skipping: {}", window);
284
continue;
285
};
286
let view_format = surface_data
287
.texture_view_format
288
.unwrap_or(surface_data.configuration.format);
289
let size = Extent3d {
290
width: surface_data.configuration.width,
291
height: surface_data.configuration.height,
292
..default()
293
};
294
let (texture_view, state) = prepare_screenshot_state(
295
size,
296
view_format,
297
&render_device,
298
&screenshot_pipeline,
299
&pipeline_cache,
300
&mut pipelines,
301
);
302
prepared.insert(*entity, state);
303
view_target_attachments.insert(
304
target.clone(),
305
OutputColorAttachment::new(texture_view.clone(), view_format),
306
);
307
}
308
NormalizedRenderTarget::Image(image) => {
309
let Some(gpu_image) = images.get(&image.handle) else {
310
warn!("Unknown image for screenshot, skipping: {:?}", image);
311
continue;
312
};
313
let view_format = gpu_image.view_format();
314
let (texture_view, state) = prepare_screenshot_state(
315
gpu_image.texture_descriptor.size,
316
view_format,
317
&render_device,
318
&screenshot_pipeline,
319
&pipeline_cache,
320
&mut pipelines,
321
);
322
prepared.insert(*entity, state);
323
view_target_attachments.insert(
324
target.clone(),
325
OutputColorAttachment::new(texture_view.clone(), view_format),
326
);
327
}
328
NormalizedRenderTarget::TextureView(texture_view) => {
329
let Some(manual_texture_view) = manual_texture_views.get(texture_view) else {
330
warn!(
331
"Unknown manual texture view for screenshot, skipping: {:?}",
332
texture_view
333
);
334
continue;
335
};
336
let view_format = manual_texture_view.view_format;
337
let size = manual_texture_view.size.to_extents();
338
let (texture_view, state) = prepare_screenshot_state(
339
size,
340
view_format,
341
&render_device,
342
&screenshot_pipeline,
343
&pipeline_cache,
344
&mut pipelines,
345
);
346
prepared.insert(*entity, state);
347
view_target_attachments.insert(
348
target.clone(),
349
OutputColorAttachment::new(texture_view.clone(), view_format),
350
);
351
}
352
NormalizedRenderTarget::None { .. } => {
353
// Nothing to screenshot!
354
}
355
}
356
}
357
}
358
359
fn prepare_screenshot_state(
360
size: Extent3d,
361
format: TextureFormat,
362
render_device: &RenderDevice,
363
pipeline: &ScreenshotToScreenPipeline,
364
pipeline_cache: &PipelineCache,
365
pipelines: &mut SpecializedRenderPipelines<ScreenshotToScreenPipeline>,
366
) -> (TextureView, ScreenshotPreparedState) {
367
let texture = render_device.create_texture(&wgpu::TextureDescriptor {
368
label: Some("screenshot-capture-rendertarget"),
369
size,
370
mip_level_count: 1,
371
sample_count: 1,
372
dimension: wgpu::TextureDimension::D2,
373
format,
374
usage: TextureUsages::RENDER_ATTACHMENT
375
| TextureUsages::COPY_SRC
376
| TextureUsages::TEXTURE_BINDING,
377
view_formats: &[],
378
});
379
let texture_view = texture.create_view(&Default::default());
380
let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
381
label: Some("screenshot-transfer-buffer"),
382
size: gpu_readback::get_aligned_size(size, format.pixel_size().unwrap_or(0) as u32) as u64,
383
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
384
mapped_at_creation: false,
385
});
386
let bind_group = render_device.create_bind_group(
387
"screenshot-to-screen-bind-group",
388
&pipeline_cache.get_bind_group_layout(&pipeline.bind_group_layout),
389
&BindGroupEntries::single(&texture_view),
390
);
391
let pipeline_id = pipelines.specialize(pipeline_cache, pipeline, format);
392
393
(
394
texture_view,
395
ScreenshotPreparedState {
396
texture,
397
buffer,
398
bind_group,
399
pipeline_id,
400
size,
401
},
402
)
403
}
404
405
pub struct ScreenshotPlugin;
406
407
impl Plugin for ScreenshotPlugin {
408
fn build(&self, app: &mut bevy_app::App) {
409
embedded_asset!(app, "screenshot.wgsl");
410
411
let (tx, rx) = std::sync::mpsc::channel();
412
app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx))))
413
.add_systems(
414
First,
415
clear_screenshots
416
.after(message_update_system)
417
.before(ApplyDeferred),
418
)
419
.add_systems(Update, trigger_screenshots);
420
421
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
422
return;
423
};
424
425
render_app
426
.insert_resource(RenderScreenshotsSender(tx))
427
.init_resource::<RenderScreenshotTargets>()
428
.init_resource::<RenderScreenshotsPrepared>()
429
.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>()
430
.add_systems(RenderStartup, init_screenshot_to_screen_pipeline)
431
.add_systems(ExtractSchedule, extract_screenshots.ambiguous_with_all())
432
.add_systems(
433
Render,
434
prepare_screenshots
435
.after(prepare_view_attachments)
436
.before(prepare_view_targets)
437
.in_set(RenderSystems::ManageViews),
438
);
439
}
440
}
441
442
#[derive(Resource)]
443
pub struct ScreenshotToScreenPipeline {
444
pub bind_group_layout: BindGroupLayoutDescriptor,
445
pub shader: Handle<Shader>,
446
}
447
448
pub fn init_screenshot_to_screen_pipeline(mut commands: Commands, asset_server: Res<AssetServer>) {
449
let bind_group_layout = BindGroupLayoutDescriptor::new(
450
"screenshot-to-screen-bgl",
451
&BindGroupLayoutEntries::single(
452
wgpu::ShaderStages::FRAGMENT,
453
texture_2d(wgpu::TextureSampleType::Float { filterable: false }),
454
),
455
);
456
457
let shader = load_embedded_asset!(asset_server.as_ref(), "screenshot.wgsl");
458
459
commands.insert_resource(ScreenshotToScreenPipeline {
460
bind_group_layout,
461
shader,
462
});
463
}
464
465
impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {
466
type Key = TextureFormat;
467
468
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
469
RenderPipelineDescriptor {
470
label: Some(Cow::Borrowed("screenshot-to-screen")),
471
layout: vec![self.bind_group_layout.clone()],
472
vertex: VertexState {
473
shader: self.shader.clone(),
474
..default()
475
},
476
primitive: wgpu::PrimitiveState {
477
cull_mode: Some(wgpu::Face::Back),
478
..Default::default()
479
},
480
multisample: Default::default(),
481
fragment: Some(FragmentState {
482
shader: self.shader.clone(),
483
targets: vec![Some(wgpu::ColorTargetState {
484
format: key,
485
blend: None,
486
write_mask: wgpu::ColorWrites::ALL,
487
})],
488
..default()
489
}),
490
..default()
491
}
492
}
493
}
494
495
pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) {
496
let targets = world.resource::<RenderScreenshotTargets>();
497
let prepared = world.resource::<RenderScreenshotsPrepared>();
498
let pipelines = world.resource::<PipelineCache>();
499
let gpu_images = world.resource::<RenderAssets<GpuImage>>();
500
let windows = world.resource::<ExtractedWindows>();
501
let manual_texture_views = world.resource::<ManualTextureViews>();
502
503
for (entity, render_target) in targets.iter() {
504
match render_target {
505
NormalizedRenderTarget::Window(window) => {
506
let window = window.entity();
507
let Some(window) = windows.get(&window) else {
508
continue;
509
};
510
let width = window.physical_width;
511
let height = window.physical_height;
512
let Some(texture_format) = window.swap_chain_texture_view_format else {
513
continue;
514
};
515
let Some(swap_chain_texture_view) = window.swap_chain_texture_view.as_ref() else {
516
continue;
517
};
518
render_screenshot(
519
encoder,
520
prepared,
521
pipelines,
522
entity,
523
width,
524
height,
525
texture_format,
526
swap_chain_texture_view,
527
);
528
}
529
NormalizedRenderTarget::Image(image) => {
530
let Some(gpu_image) = gpu_images.get(&image.handle) else {
531
warn!("Unknown image for screenshot, skipping: {:?}", image);
532
continue;
533
};
534
let width = gpu_image.texture_descriptor.size.width;
535
let height = gpu_image.texture_descriptor.size.height;
536
let texture_format = gpu_image.texture_descriptor.format;
537
let texture_view = gpu_image.texture_view.deref();
538
render_screenshot(
539
encoder,
540
prepared,
541
pipelines,
542
entity,
543
width,
544
height,
545
texture_format,
546
texture_view,
547
);
548
}
549
NormalizedRenderTarget::TextureView(texture_view) => {
550
let Some(texture_view) = manual_texture_views.get(texture_view) else {
551
warn!(
552
"Unknown manual texture view for screenshot, skipping: {:?}",
553
texture_view
554
);
555
continue;
556
};
557
let width = texture_view.size.x;
558
let height = texture_view.size.y;
559
let texture_format = texture_view.view_format;
560
let texture_view = texture_view.texture_view.deref();
561
render_screenshot(
562
encoder,
563
prepared,
564
pipelines,
565
entity,
566
width,
567
height,
568
texture_format,
569
texture_view,
570
);
571
}
572
NormalizedRenderTarget::None { .. } => {
573
// Nothing to screenshot!
574
}
575
};
576
}
577
}
578
579
fn render_screenshot(
580
encoder: &mut CommandEncoder,
581
prepared: &RenderScreenshotsPrepared,
582
pipelines: &PipelineCache,
583
entity: &Entity,
584
width: u32,
585
height: u32,
586
texture_format: TextureFormat,
587
texture_view: &wgpu::TextureView,
588
) {
589
if let Some(prepared_state) = &prepared.get(entity) {
590
let extent = Extent3d {
591
width,
592
height,
593
depth_or_array_layers: 1,
594
};
595
encoder.copy_texture_to_buffer(
596
prepared_state.texture.as_image_copy(),
597
wgpu::TexelCopyBufferInfo {
598
buffer: &prepared_state.buffer,
599
layout: gpu_readback::layout_data(extent, texture_format),
600
},
601
extent,
602
);
603
604
if let Some(pipeline) = pipelines.get_render_pipeline(prepared_state.pipeline_id) {
605
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
606
label: Some("screenshot_to_screen_pass"),
607
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
608
view: texture_view,
609
depth_slice: None,
610
resolve_target: None,
611
ops: wgpu::Operations {
612
load: wgpu::LoadOp::Load,
613
store: wgpu::StoreOp::Store,
614
},
615
})],
616
depth_stencil_attachment: None,
617
timestamp_writes: None,
618
occlusion_query_set: None,
619
multiview_mask: None,
620
});
621
pass.set_pipeline(pipeline);
622
pass.set_bind_group(0, &prepared_state.bind_group, &[]);
623
pass.draw(0..3, 0..1);
624
}
625
}
626
}
627
628
pub(crate) fn collect_screenshots(world: &mut World) {
629
#[cfg(feature = "trace")]
630
let _span = bevy_log::info_span!("collect_screenshots").entered();
631
632
let sender = world.resource::<RenderScreenshotsSender>().deref().clone();
633
let prepared = world.resource::<RenderScreenshotsPrepared>();
634
635
for (entity, prepared) in prepared.iter() {
636
let entity = *entity;
637
let sender = sender.clone();
638
let width = prepared.size.width;
639
let height = prepared.size.height;
640
let texture_format = prepared.texture.format();
641
let Ok(pixel_size) = texture_format.pixel_size() else {
642
continue;
643
};
644
let buffer = prepared.buffer.clone();
645
646
let finish = async move {
647
let (tx, rx) = async_channel::bounded(1);
648
let buffer_slice = buffer.slice(..);
649
// The polling for this map call is done every frame when the command queue is submitted.
650
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
651
if let Err(err) = result {
652
panic!("{}", err.to_string());
653
}
654
tx.try_send(()).unwrap();
655
});
656
rx.recv().await.unwrap();
657
let data = buffer_slice.get_mapped_range();
658
// we immediately move the data to CPU memory to avoid holding the mapped view for long
659
let mut result = Vec::from(&*data);
660
drop(data);
661
662
if result.len() != ((width * height) as usize * pixel_size) {
663
// Our buffer has been padded because we needed to align to a multiple of 256.
664
// We remove this padding here
665
let initial_row_bytes = width as usize * pixel_size;
666
let buffered_row_bytes =
667
gpu_readback::align_byte_size(width * pixel_size as u32) as usize;
668
669
let mut take_offset = buffered_row_bytes;
670
let mut place_offset = initial_row_bytes;
671
for _ in 1..height {
672
result.copy_within(take_offset..take_offset + buffered_row_bytes, place_offset);
673
take_offset += buffered_row_bytes;
674
place_offset += initial_row_bytes;
675
}
676
result.truncate(initial_row_bytes * height as usize);
677
}
678
679
if let Err(e) = sender.send((
680
entity,
681
Image::new(
682
Extent3d {
683
width,
684
height,
685
depth_or_array_layers: 1,
686
},
687
wgpu::TextureDimension::D2,
688
result,
689
texture_format,
690
RenderAssetUsages::RENDER_WORLD,
691
),
692
)) {
693
error!("Failed to send screenshot: {}", e);
694
}
695
};
696
697
AsyncComputeTaskPool::get().spawn(finish).detach();
698
}
699
}
700
701