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