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