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