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#[derive(Component, Deref, DerefMut, Reflect, Debug)]
73#[reflect(Component, Debug)]
74pub struct Screenshot(pub RenderTarget);
75
76#[derive(Component)]
78pub struct Capturing;
79
80#[derive(Component)]
83pub struct Captured;
84
85impl Screenshot {
86 pub fn window(window: Entity) -> Self {
88 Self(RenderTarget::Window(WindowRef::Entity(window)))
89 }
90
91 pub fn primary_window() -> Self {
93 Self(RenderTarget::Window(WindowRef::Primary))
94 }
95
96 pub fn image(image: Handle<Image>) -> Self {
98 Self(RenderTarget::Image(image))
99 }
100
101 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
127pub 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 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 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 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 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 let mut result = Vec::from(&*data);
665 drop(data);
666
667 if result.len() != ((width * height) as usize * pixel_size) {
668 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}