1use std::{borrow::Cow, path::Path, sync::PoisonError};
2
3use bevy_app::Plugin;
4use bevy_asset::{load_internal_asset, Handle};
5use bevy_ecs::{entity::EntityHashMap, prelude::*};
6use bevy_tasks::AsyncComputeTaskPool;
7use bevy_utils::tracing::{error, info, info_span};
8use std::sync::Mutex;
9use thiserror::Error;
10use wgpu::{
11 CommandEncoder, Extent3d, ImageDataLayout, TextureFormat, COPY_BYTES_PER_ROW_ALIGNMENT,
12};
13
14use crate::{
15 prelude::{Image, Shader},
16 render_asset::RenderAssetUsages,
17 render_resource::{
18 binding_types::texture_2d, BindGroup, BindGroupLayout, BindGroupLayoutEntries, Buffer,
19 CachedRenderPipelineId, FragmentState, PipelineCache, RenderPipelineDescriptor,
20 SpecializedRenderPipeline, SpecializedRenderPipelines, Texture, VertexState,
21 },
22 renderer::RenderDevice,
23 texture::TextureFormatPixelInfo,
24 RenderApp,
25};
26
27use super::ExtractedWindows;
28
29pub type ScreenshotFn = Box<dyn FnOnce(Image) + Send + Sync>;
30
31#[derive(Resource, Default)]
33pub struct ScreenshotManager {
34 pub(crate) callbacks: Mutex<EntityHashMap<ScreenshotFn>>,
36}
37
38#[derive(Error, Debug)]
39#[error("A screenshot for this window has already been requested.")]
40pub struct ScreenshotAlreadyRequestedError;
41
42impl ScreenshotManager {
43 pub fn take_screenshot(
47 &mut self,
48 window: Entity,
49 callback: impl FnOnce(Image) + Send + Sync + 'static,
50 ) -> Result<(), ScreenshotAlreadyRequestedError> {
51 self.callbacks
52 .get_mut()
53 .unwrap_or_else(PoisonError::into_inner)
54 .try_insert(window, Box::new(callback))
55 .map(|_| ())
56 .map_err(|_| ScreenshotAlreadyRequestedError)
57 }
58
59 pub fn save_screenshot_to_disk(
63 &mut self,
64 window: Entity,
65 path: impl AsRef<Path>,
66 ) -> Result<(), ScreenshotAlreadyRequestedError> {
67 let path = path.as_ref().to_owned();
68 self.take_screenshot(window, move |img| match img.try_into_dynamic() {
69 Ok(dyn_img) => match image::ImageFormat::from_path(&path) {
70 Ok(format) => {
71 let img = dyn_img.to_rgb8();
74 #[cfg(not(target_arch = "wasm32"))]
75 match img.save_with_format(&path, format) {
76 Ok(_) => info!("Screenshot saved to {}", path.display()),
77 Err(e) => error!("Cannot save screenshot, IO error: {e}"),
78 }
79
80 #[cfg(target_arch = "wasm32")]
81 {
82 let save_screenshot = || {
83 use image::EncodableLayout;
84 use wasm_bindgen::{JsCast, JsValue};
85
86 let mut image_buffer = std::io::Cursor::new(Vec::new());
87 img.write_to(&mut image_buffer, format)
88 .map_err(|e| JsValue::from_str(&format!("{e}")))?;
89 let parts = js_sys::Array::of1(&unsafe {
91 js_sys::Uint8Array::view(image_buffer.into_inner().as_bytes())
92 .into()
93 });
94 let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)?;
95 let url = web_sys::Url::create_object_url_with_blob(&blob)?;
96 let window = web_sys::window().unwrap();
97 let document = window.document().unwrap();
98 let link = document.create_element("a")?;
99 link.set_attribute("href", &url)?;
100 link.set_attribute(
101 "download",
102 path.file_name()
103 .and_then(|filename| filename.to_str())
104 .ok_or_else(|| JsValue::from_str("Invalid filename"))?,
105 )?;
106 let html_element = link.dyn_into::<web_sys::HtmlElement>()?;
107 html_element.click();
108 web_sys::Url::revoke_object_url(&url)?;
109 Ok::<(), JsValue>(())
110 };
111
112 match (save_screenshot)() {
113 Ok(_) => info!("Screenshot saved to {}", path.display()),
114 Err(e) => error!("Cannot save screenshot, error: {e:?}"),
115 };
116 }
117 }
118 Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"),
119 },
120 Err(e) => error!("Cannot save screenshot, screen format cannot be understood: {e}"),
121 })
122 }
123}
124
125pub struct ScreenshotPlugin;
126
127const SCREENSHOT_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(11918575842344596158);
128
129impl Plugin for ScreenshotPlugin {
130 fn build(&self, app: &mut bevy_app::App) {
131 app.init_resource::<ScreenshotManager>();
132
133 load_internal_asset!(
134 app,
135 SCREENSHOT_SHADER_HANDLE,
136 "screenshot.wgsl",
137 Shader::from_wgsl
138 );
139 }
140
141 fn finish(&self, app: &mut bevy_app::App) {
142 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
143 render_app.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>();
144 }
145 }
146}
147
148pub(crate) fn align_byte_size(value: u32) -> u32 {
149 value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT))
150}
151
152pub(crate) fn get_aligned_size(width: u32, height: u32, pixel_size: u32) -> u32 {
153 height * align_byte_size(width * pixel_size)
154}
155
156pub(crate) fn layout_data(width: u32, height: u32, format: TextureFormat) -> ImageDataLayout {
157 ImageDataLayout {
158 bytes_per_row: if height > 1 {
159 Some(get_aligned_size(width, 1, format.pixel_size() as u32))
161 } else {
162 None
163 },
164 rows_per_image: None,
165 ..Default::default()
166 }
167}
168
169#[derive(Resource)]
170pub struct ScreenshotToScreenPipeline {
171 pub bind_group_layout: BindGroupLayout,
172}
173
174impl FromWorld for ScreenshotToScreenPipeline {
175 fn from_world(render_world: &mut World) -> Self {
176 let device = render_world.resource::<RenderDevice>();
177
178 let bind_group_layout = device.create_bind_group_layout(
179 "screenshot-to-screen-bgl",
180 &BindGroupLayoutEntries::single(
181 wgpu::ShaderStages::FRAGMENT,
182 texture_2d(wgpu::TextureSampleType::Float { filterable: false }),
183 ),
184 );
185
186 Self { bind_group_layout }
187 }
188}
189
190impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {
191 type Key = TextureFormat;
192
193 fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
194 RenderPipelineDescriptor {
195 label: Some(Cow::Borrowed("screenshot-to-screen")),
196 layout: vec![self.bind_group_layout.clone()],
197 vertex: VertexState {
198 buffers: vec![],
199 shader_defs: vec![],
200 entry_point: Cow::Borrowed("vs_main"),
201 shader: SCREENSHOT_SHADER_HANDLE,
202 },
203 primitive: wgpu::PrimitiveState {
204 cull_mode: Some(wgpu::Face::Back),
205 ..Default::default()
206 },
207 depth_stencil: None,
208 multisample: Default::default(),
209 fragment: Some(FragmentState {
210 shader: SCREENSHOT_SHADER_HANDLE,
211 entry_point: Cow::Borrowed("fs_main"),
212 shader_defs: vec![],
213 targets: vec![Some(wgpu::ColorTargetState {
214 format: key,
215 blend: None,
216 write_mask: wgpu::ColorWrites::ALL,
217 })],
218 }),
219 push_constant_ranges: Vec::new(),
220 }
221 }
222}
223
224pub struct ScreenshotPreparedState {
225 pub texture: Texture,
226 pub buffer: Buffer,
227 pub bind_group: BindGroup,
228 pub pipeline_id: CachedRenderPipelineId,
229}
230
231pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) {
232 let windows = world.resource::<ExtractedWindows>();
233 let pipelines = world.resource::<PipelineCache>();
234
235 for window in windows.values() {
236 if let Some(memory) = &window.screenshot_memory {
237 let width = window.physical_width;
238 let height = window.physical_height;
239 let texture_format = window.swap_chain_texture_format.unwrap();
240
241 encoder.copy_texture_to_buffer(
242 memory.texture.as_image_copy(),
243 wgpu::ImageCopyBuffer {
244 buffer: &memory.buffer,
245 layout: layout_data(width, height, texture_format),
246 },
247 Extent3d {
248 width,
249 height,
250 ..Default::default()
251 },
252 );
253 if let Some(pipeline) = pipelines.get_render_pipeline(memory.pipeline_id) {
254 let true_swapchain_texture_view = window
255 .swap_chain_texture
256 .as_ref()
257 .unwrap()
258 .texture
259 .create_view(&Default::default());
260 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
261 label: Some("screenshot_to_screen_pass"),
262 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
263 view: &true_swapchain_texture_view,
264 resolve_target: None,
265 ops: wgpu::Operations {
266 load: wgpu::LoadOp::Load,
267 store: wgpu::StoreOp::Store,
268 },
269 })],
270 depth_stencil_attachment: None,
271 timestamp_writes: None,
272 occlusion_query_set: None,
273 });
274 pass.set_pipeline(pipeline);
275 pass.set_bind_group(0, &memory.bind_group, &[]);
276 pass.draw(0..3, 0..1);
277 }
278 }
279 }
280}
281
282pub(crate) fn collect_screenshots(world: &mut World) {
283 let _span = info_span!("collect_screenshots");
284
285 let mut windows = world.resource_mut::<ExtractedWindows>();
286 for window in windows.values_mut() {
287 if let Some(screenshot_func) = window.screenshot_func.take() {
288 let width = window.physical_width;
289 let height = window.physical_height;
290 let texture_format = window.swap_chain_texture_format.unwrap();
291 let pixel_size = texture_format.pixel_size();
292 let ScreenshotPreparedState { buffer, .. } = window.screenshot_memory.take().unwrap();
293
294 let finish = async move {
295 let (tx, rx) = async_channel::bounded(1);
296 let buffer_slice = buffer.slice(..);
297 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
299 let err = result.err();
300 if err.is_some() {
301 panic!("{}", err.unwrap().to_string());
302 }
303 tx.try_send(()).unwrap();
304 });
305 rx.recv().await.unwrap();
306 let data = buffer_slice.get_mapped_range();
307 let mut result = Vec::from(&*data);
309 drop(data);
310 drop(buffer);
311
312 if result.len() != ((width * height) as usize * pixel_size) {
313 let initial_row_bytes = width as usize * pixel_size;
316 let buffered_row_bytes = align_byte_size(width * pixel_size as u32) as usize;
317
318 let mut take_offset = buffered_row_bytes;
319 let mut place_offset = initial_row_bytes;
320 for _ in 1..height {
321 result.copy_within(
322 take_offset..take_offset + buffered_row_bytes,
323 place_offset,
324 );
325 take_offset += buffered_row_bytes;
326 place_offset += initial_row_bytes;
327 }
328 result.truncate(initial_row_bytes * height as usize);
329 }
330
331 screenshot_func(Image::new(
332 Extent3d {
333 width,
334 height,
335 depth_or_array_layers: 1,
336 },
337 wgpu::TextureDimension::D2,
338 result,
339 texture_format,
340 RenderAssetUsages::RENDER_WORLD,
341 ));
342 };
343
344 AsyncComputeTaskPool::get().spawn(finish).detach();
345 }
346 }
347}