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