bevy_render/view/window/
screenshot.rs

1use super::ExtractedWindows;
2use crate::{
3    camera::{ManualTextureViewHandle, ManualTextureViews, NormalizedRenderTarget, RenderTarget},
4    gpu_readback,
5    prelude::Shader,
6    render_asset::{RenderAssetUsages, RenderAssets},
7    render_resource::{
8        binding_types::texture_2d, BindGroup, BindGroupEntries, BindGroupLayout,
9        BindGroupLayoutEntries, Buffer, BufferUsages, CachedRenderPipelineId, FragmentState,
10        PipelineCache, RenderPipelineDescriptor, SpecializedRenderPipeline,
11        SpecializedRenderPipelines, Texture, TextureUsages, TextureView, VertexState,
12    },
13    renderer::RenderDevice,
14    texture::{GpuImage, OutputColorAttachment},
15    view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces},
16    ExtractSchedule, MainWorld, Render, RenderApp, RenderSet,
17};
18use alloc::{borrow::Cow, sync::Arc};
19use bevy_app::{First, Plugin, Update};
20use bevy_asset::{load_internal_asset, weak_handle, Handle};
21use bevy_derive::{Deref, DerefMut};
22use bevy_ecs::{
23    entity::EntityHashMap, event::event_update_system, prelude::*, system::SystemState,
24};
25use bevy_image::{Image, TextureFormatPixelInfo};
26use bevy_platform::collections::HashSet;
27use bevy_reflect::Reflect;
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(Event, Deref, DerefMut, Reflect, Debug)]
43#[reflect(Debug)]
44pub 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)]
71pub struct Screenshot(pub RenderTarget);
72
73/// A marker component that indicates that a screenshot is currently being captured.
74#[derive(Component, Default)]
75pub 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)]
80pub struct Captured;
81
82impl 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
104struct 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)]
113pub struct CapturedScreenshots(pub Arc<Mutex<Receiver<(Entity, Image)>>>);
114
115#[derive(Resource, Deref, DerefMut, Default)]
116struct RenderScreenshotTargets(EntityHashMap<NormalizedRenderTarget>);
117
118#[derive(Resource, Deref, DerefMut, Default)]
119struct RenderScreenshotsPrepared(EntityHashMap<ScreenshotPreparedState>);
120
121#[derive(Resource, Deref, DerefMut)]
122struct RenderScreenshotsSender(Sender<(Entity, Image)>);
123
124/// Saves the captured screenshot to disk at the provided path.
125pub fn save_to_disk(path: impl AsRef<Path>) -> impl FnMut(Trigger<ScreenshotCaptured>) {
126    let path = path.as_ref().to_owned();
127    move |trigger| {
128        let img = trigger.event().deref().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
186fn clear_screenshots(mut commands: Commands, screenshots: Query<Entity, With<Captured>>) {
187    for entity in screenshots.iter() {
188        commands.entity(entity).despawn();
189    }
190}
191
192pub 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
203fn 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
254fn 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 = Extent3d {
325                    width: manual_texture_view.size.x,
326                    height: manual_texture_view.size.y,
327                    ..default()
328                };
329                let (texture_view, state) = prepare_screenshot_state(
330                    size,
331                    format,
332                    &render_device,
333                    &screenshot_pipeline,
334                    &pipeline_cache,
335                    &mut pipelines,
336                );
337                prepared.insert(*entity, state);
338                view_target_attachments.insert(
339                    target.clone(),
340                    OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()),
341                );
342            }
343        }
344    }
345}
346
347fn prepare_screenshot_state(
348    size: Extent3d,
349    format: TextureFormat,
350    render_device: &RenderDevice,
351    pipeline: &ScreenshotToScreenPipeline,
352    pipeline_cache: &PipelineCache,
353    pipelines: &mut SpecializedRenderPipelines<ScreenshotToScreenPipeline>,
354) -> (TextureView, ScreenshotPreparedState) {
355    let texture = render_device.create_texture(&wgpu::TextureDescriptor {
356        label: Some("screenshot-capture-rendertarget"),
357        size,
358        mip_level_count: 1,
359        sample_count: 1,
360        dimension: wgpu::TextureDimension::D2,
361        format,
362        usage: TextureUsages::RENDER_ATTACHMENT
363            | TextureUsages::COPY_SRC
364            | TextureUsages::TEXTURE_BINDING,
365        view_formats: &[],
366    });
367    let texture_view = texture.create_view(&Default::default());
368    let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
369        label: Some("screenshot-transfer-buffer"),
370        size: gpu_readback::get_aligned_size(size, format.pixel_size() as u32) as u64,
371        usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
372        mapped_at_creation: false,
373    });
374    let bind_group = render_device.create_bind_group(
375        "screenshot-to-screen-bind-group",
376        &pipeline.bind_group_layout,
377        &BindGroupEntries::single(&texture_view),
378    );
379    let pipeline_id = pipelines.specialize(pipeline_cache, pipeline, format);
380
381    (
382        texture_view,
383        ScreenshotPreparedState {
384            texture,
385            buffer,
386            bind_group,
387            pipeline_id,
388            size,
389        },
390    )
391}
392
393pub struct ScreenshotPlugin;
394
395const SCREENSHOT_SHADER_HANDLE: Handle<Shader> =
396    weak_handle!("c31753d6-326a-47cb-a359-65c97a471fda");
397
398impl Plugin for ScreenshotPlugin {
399    fn build(&self, app: &mut bevy_app::App) {
400        app.add_systems(
401            First,
402            clear_screenshots
403                .after(event_update_system)
404                .before(ApplyDeferred),
405        )
406        .add_systems(Update, trigger_screenshots)
407        .register_type::<Screenshot>()
408        .register_type::<ScreenshotCaptured>();
409
410        load_internal_asset!(
411            app,
412            SCREENSHOT_SHADER_HANDLE,
413            "screenshot.wgsl",
414            Shader::from_wgsl
415        );
416    }
417
418    fn finish(&self, app: &mut bevy_app::App) {
419        let (tx, rx) = std::sync::mpsc::channel();
420        app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx))));
421
422        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
423            render_app
424                .insert_resource(RenderScreenshotsSender(tx))
425                .init_resource::<RenderScreenshotTargets>()
426                .init_resource::<RenderScreenshotsPrepared>()
427                .init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>()
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(RenderSet::ManageViews),
435                );
436        }
437    }
438}
439
440#[derive(Resource)]
441pub struct ScreenshotToScreenPipeline {
442    pub bind_group_layout: BindGroupLayout,
443}
444
445impl FromWorld for ScreenshotToScreenPipeline {
446    fn from_world(render_world: &mut World) -> Self {
447        let device = render_world.resource::<RenderDevice>();
448
449        let bind_group_layout = device.create_bind_group_layout(
450            "screenshot-to-screen-bgl",
451            &BindGroupLayoutEntries::single(
452                wgpu::ShaderStages::FRAGMENT,
453                texture_2d(wgpu::TextureSampleType::Float { filterable: false }),
454            ),
455        );
456
457        Self { bind_group_layout }
458    }
459}
460
461impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {
462    type Key = TextureFormat;
463
464    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
465        RenderPipelineDescriptor {
466            label: Some(Cow::Borrowed("screenshot-to-screen")),
467            layout: vec![self.bind_group_layout.clone()],
468            vertex: VertexState {
469                buffers: vec![],
470                shader_defs: vec![],
471                entry_point: Cow::Borrowed("vs_main"),
472                shader: SCREENSHOT_SHADER_HANDLE,
473            },
474            primitive: wgpu::PrimitiveState {
475                cull_mode: Some(wgpu::Face::Back),
476                ..Default::default()
477            },
478            depth_stencil: None,
479            multisample: Default::default(),
480            fragment: Some(FragmentState {
481                shader: SCREENSHOT_SHADER_HANDLE,
482                entry_point: Cow::Borrowed("fs_main"),
483                shader_defs: vec![],
484                targets: vec![Some(wgpu::ColorTargetState {
485                    format: key,
486                    blend: None,
487                    write_mask: wgpu::ColorWrites::ALL,
488                })],
489            }),
490            push_constant_ranges: Vec::new(),
491            zero_initialize_workgroup_memory: false,
492        }
493    }
494}
495
496pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) {
497    let targets = world.resource::<RenderScreenshotTargets>();
498    let prepared = world.resource::<RenderScreenshotsPrepared>();
499    let pipelines = world.resource::<PipelineCache>();
500    let gpu_images = world.resource::<RenderAssets<GpuImage>>();
501    let windows = world.resource::<ExtractedWindows>();
502    let manual_texture_views = world.resource::<ManualTextureViews>();
503
504    for (entity, render_target) in targets.iter() {
505        match render_target {
506            NormalizedRenderTarget::Window(window) => {
507                let window = window.entity();
508                let Some(window) = windows.get(&window) else {
509                    continue;
510                };
511                let width = window.physical_width;
512                let height = window.physical_height;
513                let Some(texture_format) = window.swap_chain_texture_format else {
514                    continue;
515                };
516                let Some(swap_chain_texture) = window.swap_chain_texture.as_ref() else {
517                    continue;
518                };
519                let texture_view = swap_chain_texture.texture.create_view(&Default::default());
520                render_screenshot(
521                    encoder,
522                    prepared,
523                    pipelines,
524                    entity,
525                    width,
526                    height,
527                    texture_format,
528                    &texture_view,
529                );
530            }
531            NormalizedRenderTarget::Image(image) => {
532                let Some(gpu_image) = gpu_images.get(&image.handle) else {
533                    warn!("Unknown image for screenshot, skipping: {:?}", image);
534                    continue;
535                };
536                let width = gpu_image.size.width;
537                let height = gpu_image.size.height;
538                let texture_format = gpu_image.texture_format;
539                let texture_view = gpu_image.texture_view.deref();
540                render_screenshot(
541                    encoder,
542                    prepared,
543                    pipelines,
544                    entity,
545                    width,
546                    height,
547                    texture_format,
548                    texture_view,
549                );
550            }
551            NormalizedRenderTarget::TextureView(texture_view) => {
552                let Some(texture_view) = manual_texture_views.get(texture_view) else {
553                    warn!(
554                        "Unknown manual texture view for screenshot, skipping: {:?}",
555                        texture_view
556                    );
557                    continue;
558                };
559                let width = texture_view.size.x;
560                let height = texture_view.size.y;
561                let texture_format = texture_view.format;
562                let texture_view = texture_view.texture_view.deref();
563                render_screenshot(
564                    encoder,
565                    prepared,
566                    pipelines,
567                    entity,
568                    width,
569                    height,
570                    texture_format,
571                    texture_view,
572                );
573            }
574        };
575    }
576}
577
578fn render_screenshot(
579    encoder: &mut CommandEncoder,
580    prepared: &RenderScreenshotsPrepared,
581    pipelines: &PipelineCache,
582    entity: &Entity,
583    width: u32,
584    height: u32,
585    texture_format: TextureFormat,
586    texture_view: &wgpu::TextureView,
587) {
588    if let Some(prepared_state) = &prepared.get(entity) {
589        let extent = Extent3d {
590            width,
591            height,
592            depth_or_array_layers: 1,
593        };
594        encoder.copy_texture_to_buffer(
595            prepared_state.texture.as_image_copy(),
596            wgpu::TexelCopyBufferInfo {
597                buffer: &prepared_state.buffer,
598                layout: gpu_readback::layout_data(extent, texture_format),
599            },
600            extent,
601        );
602
603        if let Some(pipeline) = pipelines.get_render_pipeline(prepared_state.pipeline_id) {
604            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
605                label: Some("screenshot_to_screen_pass"),
606                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
607                    view: texture_view,
608                    resolve_target: None,
609                    ops: wgpu::Operations {
610                        load: wgpu::LoadOp::Load,
611                        store: wgpu::StoreOp::Store,
612                    },
613                })],
614                depth_stencil_attachment: None,
615                timestamp_writes: None,
616                occlusion_query_set: None,
617            });
618            pass.set_pipeline(pipeline);
619            pass.set_bind_group(0, &prepared_state.bind_group, &[]);
620            pass.draw(0..3, 0..1);
621        }
622    }
623}
624
625pub(crate) fn collect_screenshots(world: &mut World) {
626    #[cfg(feature = "trace")]
627    let _span = tracing::info_span!("collect_screenshots").entered();
628
629    let sender = world.resource::<RenderScreenshotsSender>().deref().clone();
630    let prepared = world.resource::<RenderScreenshotsPrepared>();
631
632    for (entity, prepared) in prepared.iter() {
633        let entity = *entity;
634        let sender = sender.clone();
635        let width = prepared.size.width;
636        let height = prepared.size.height;
637        let texture_format = prepared.texture.format();
638        let pixel_size = texture_format.pixel_size();
639        let buffer = prepared.buffer.clone();
640
641        let finish = async move {
642            let (tx, rx) = async_channel::bounded(1);
643            let buffer_slice = buffer.slice(..);
644            // The polling for this map call is done every frame when the command queue is submitted.
645            buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
646                let err = result.err();
647                if err.is_some() {
648                    panic!("{}", err.unwrap().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}