bevy_render/view/window/
screenshot.rs

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