Skip to main content

bevy_render/view/window/
screenshot.rs

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