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#[derive(Component, Deref, DerefMut, Reflect, Debug)]
79#[reflect(Component, Debug)]
80pub struct Screenshot(pub RenderTarget);
81
82#[derive(Component, Default)]
84pub struct Capturing;
85
86#[derive(Component, Default)]
89pub struct Captured;
90
91impl Screenshot {
92 pub fn window(window: Entity) -> Self {
94 Self(RenderTarget::Window(WindowRef::Entity(window)))
95 }
96
97 pub fn primary_window() -> Self {
99 Self(RenderTarget::Window(WindowRef::Primary))
100 }
101
102 pub fn image(image: Handle<Image>) -> Self {
104 Self(RenderTarget::Image(image.into()))
105 }
106
107 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
133pub 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 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 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 }
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 }
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 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 let mut result = Vec::from(&*data);
663 drop(data);
664
665 if result.len() != ((width * height) as usize * pixel_size) {
666 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}