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