Path: blob/main/crates/bevy_render/src/view/window/screenshot.rs
6598 views
use super::ExtractedWindows;1use crate::{2gpu_readback,3render_asset::RenderAssets,4render_resource::{5binding_types::texture_2d, BindGroup, BindGroupEntries, BindGroupLayout,6BindGroupLayoutEntries, Buffer, BufferUsages, CachedRenderPipelineId, FragmentState,7PipelineCache, RenderPipelineDescriptor, SpecializedRenderPipeline,8SpecializedRenderPipelines, Texture, TextureUsages, TextureView, VertexState,9},10renderer::RenderDevice,11texture::{GpuImage, ManualTextureViews, OutputColorAttachment},12view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces},13ExtractSchedule, MainWorld, Render, RenderApp, RenderStartup, RenderSystems,14};15use alloc::{borrow::Cow, sync::Arc};16use bevy_app::{First, Plugin, Update};17use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle, RenderAssetUsages};18use bevy_camera::{ManualTextureViewHandle, NormalizedRenderTarget, RenderTarget};19use bevy_derive::{Deref, DerefMut};20use bevy_ecs::{21entity::EntityHashMap, event::event_update_system, prelude::*, system::SystemState,22};23use bevy_image::{Image, TextureFormatPixelInfo, ToExtents};24use bevy_platform::collections::HashSet;25use bevy_reflect::Reflect;26use bevy_shader::Shader;27use bevy_tasks::AsyncComputeTaskPool;28use bevy_utils::default;29use bevy_window::{PrimaryWindow, WindowRef};30use core::ops::Deref;31use std::{32path::Path,33sync::{34mpsc::{Receiver, Sender},35Mutex,36},37};38use tracing::{error, info, warn};39use wgpu::{CommandEncoder, Extent3d, TextureFormat};4041#[derive(EntityEvent, Deref, DerefMut, Reflect, Debug)]42#[reflect(Debug)]43pub struct ScreenshotCaptured(pub Image);4445/// A component that signals to the renderer to capture a screenshot this frame.46///47/// This component should be spawned on a new entity with an observer that will trigger48/// with [`ScreenshotCaptured`] when the screenshot is ready.49///50/// Screenshots are captured asynchronously and may not be available immediately after the frame51/// that the component is spawned on. The observer should be used to handle the screenshot when it52/// is ready.53///54/// Note that the screenshot entity will be despawned after the screenshot is captured and the55/// observer is triggered.56///57/// # Usage58///59/// ```60/// # use bevy_ecs::prelude::*;61/// # use bevy_render::view::screenshot::{save_to_disk, Screenshot};62///63/// fn take_screenshot(mut commands: Commands) {64/// commands.spawn(Screenshot::primary_window())65/// .observe(save_to_disk("screenshot.png"));66/// }67/// ```68#[derive(Component, Deref, DerefMut, Reflect, Debug)]69#[reflect(Component, Debug)]70pub struct Screenshot(pub RenderTarget);7172/// A marker component that indicates that a screenshot is currently being captured.73#[derive(Component, Default)]74pub struct Capturing;7576/// A marker component that indicates that a screenshot has been captured, the image is ready, and77/// the screenshot entity can be despawned.78#[derive(Component, Default)]79pub struct Captured;8081impl Screenshot {82/// Capture a screenshot of the provided window entity.83pub fn window(window: Entity) -> Self {84Self(RenderTarget::Window(WindowRef::Entity(window)))85}8687/// Capture a screenshot of the primary window, if one exists.88pub fn primary_window() -> Self {89Self(RenderTarget::Window(WindowRef::Primary))90}9192/// Capture a screenshot of the provided render target image.93pub fn image(image: Handle<Image>) -> Self {94Self(RenderTarget::Image(image.into()))95}9697/// Capture a screenshot of the provided manual texture view.98pub fn texture_view(texture_view: ManualTextureViewHandle) -> Self {99Self(RenderTarget::TextureView(texture_view))100}101}102103struct ScreenshotPreparedState {104pub texture: Texture,105pub buffer: Buffer,106pub bind_group: BindGroup,107pub pipeline_id: CachedRenderPipelineId,108pub size: Extent3d,109}110111#[derive(Resource, Deref, DerefMut)]112pub struct CapturedScreenshots(pub Arc<Mutex<Receiver<(Entity, Image)>>>);113114#[derive(Resource, Deref, DerefMut, Default)]115struct RenderScreenshotTargets(EntityHashMap<NormalizedRenderTarget>);116117#[derive(Resource, Deref, DerefMut, Default)]118struct RenderScreenshotsPrepared(EntityHashMap<ScreenshotPreparedState>);119120#[derive(Resource, Deref, DerefMut)]121struct RenderScreenshotsSender(Sender<(Entity, Image)>);122123/// Saves the captured screenshot to disk at the provided path.124pub fn save_to_disk(path: impl AsRef<Path>) -> impl FnMut(On<ScreenshotCaptured>) {125let path = path.as_ref().to_owned();126move |event| {127let img = event.0.clone();128match img.try_into_dynamic() {129Ok(dyn_img) => match image::ImageFormat::from_path(&path) {130Ok(format) => {131// discard the alpha channel which stores brightness values when HDR is enabled to make sure132// the screenshot looks right133let img = dyn_img.to_rgb8();134#[cfg(not(target_arch = "wasm32"))]135match img.save_with_format(&path, format) {136Ok(_) => info!("Screenshot saved to {}", path.display()),137Err(e) => error!("Cannot save screenshot, IO error: {e}"),138}139140#[cfg(target_arch = "wasm32")]141{142let save_screenshot = || {143use image::EncodableLayout;144use wasm_bindgen::{JsCast, JsValue};145146let mut image_buffer = std::io::Cursor::new(Vec::new());147img.write_to(&mut image_buffer, format)148.map_err(|e| JsValue::from_str(&format!("{e}")))?;149// SAFETY: `image_buffer` only exist in this closure, and is not used after this line150let parts = js_sys::Array::of1(&unsafe {151js_sys::Uint8Array::view(image_buffer.into_inner().as_bytes())152.into()153});154let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)?;155let url = web_sys::Url::create_object_url_with_blob(&blob)?;156let window = web_sys::window().unwrap();157let document = window.document().unwrap();158let link = document.create_element("a")?;159link.set_attribute("href", &url)?;160link.set_attribute(161"download",162path.file_name()163.and_then(|filename| filename.to_str())164.ok_or_else(|| JsValue::from_str("Invalid filename"))?,165)?;166let html_element = link.dyn_into::<web_sys::HtmlElement>()?;167html_element.click();168web_sys::Url::revoke_object_url(&url)?;169Ok::<(), JsValue>(())170};171172match (save_screenshot)() {173Ok(_) => info!("Screenshot saved to {}", path.display()),174Err(e) => error!("Cannot save screenshot, error: {e:?}"),175};176}177}178Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"),179},180Err(e) => error!("Cannot save screenshot, screen format cannot be understood: {e}"),181}182}183}184185fn clear_screenshots(mut commands: Commands, screenshots: Query<Entity, With<Captured>>) {186for entity in screenshots.iter() {187commands.entity(entity).despawn();188}189}190191pub fn trigger_screenshots(192mut commands: Commands,193captured_screenshots: ResMut<CapturedScreenshots>,194) {195let captured_screenshots = captured_screenshots.lock().unwrap();196while let Ok((entity, image)) = captured_screenshots.try_recv() {197commands.entity(entity).insert(Captured);198commands.trigger_targets(ScreenshotCaptured(image), entity);199}200}201202fn extract_screenshots(203mut targets: ResMut<RenderScreenshotTargets>,204mut main_world: ResMut<MainWorld>,205mut system_state: Local<206Option<207SystemState<(208Commands,209Query<Entity, With<PrimaryWindow>>,210Query<(Entity, &Screenshot), Without<Capturing>>,211)>,212>,213>,214mut seen_targets: Local<HashSet<NormalizedRenderTarget>>,215) {216if system_state.is_none() {217*system_state = Some(SystemState::new(&mut main_world));218}219let system_state = system_state.as_mut().unwrap();220let (mut commands, primary_window, screenshots) = system_state.get_mut(&mut main_world);221222targets.clear();223seen_targets.clear();224225let primary_window = primary_window.iter().next();226227for (entity, screenshot) in screenshots.iter() {228let render_target = screenshot.0.clone();229let Some(render_target) = render_target.normalize(primary_window) else {230warn!(231"Unknown render target for screenshot, skipping: {:?}",232render_target233);234continue;235};236if seen_targets.contains(&render_target) {237warn!(238"Duplicate render target for screenshot, skipping entity {}: {:?}",239entity, render_target240);241// If we don't despawn the entity here, it will be captured again in the next frame242commands.entity(entity).despawn();243continue;244}245seen_targets.insert(render_target.clone());246targets.insert(entity, render_target);247commands.entity(entity).insert(Capturing);248}249250system_state.apply(&mut main_world);251}252253fn prepare_screenshots(254targets: Res<RenderScreenshotTargets>,255mut prepared: ResMut<RenderScreenshotsPrepared>,256window_surfaces: Res<WindowSurfaces>,257render_device: Res<RenderDevice>,258screenshot_pipeline: Res<ScreenshotToScreenPipeline>,259pipeline_cache: Res<PipelineCache>,260mut pipelines: ResMut<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>,261images: Res<RenderAssets<GpuImage>>,262manual_texture_views: Res<ManualTextureViews>,263mut view_target_attachments: ResMut<ViewTargetAttachments>,264) {265prepared.clear();266for (entity, target) in targets.iter() {267match target {268NormalizedRenderTarget::Window(window) => {269let window = window.entity();270let Some(surface_data) = window_surfaces.surfaces.get(&window) else {271warn!("Unknown window for screenshot, skipping: {}", window);272continue;273};274let format = surface_data.configuration.format.add_srgb_suffix();275let size = Extent3d {276width: surface_data.configuration.width,277height: surface_data.configuration.height,278..default()279};280let (texture_view, state) = prepare_screenshot_state(281size,282format,283&render_device,284&screenshot_pipeline,285&pipeline_cache,286&mut pipelines,287);288prepared.insert(*entity, state);289view_target_attachments.insert(290target.clone(),291OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()),292);293}294NormalizedRenderTarget::Image(image) => {295let Some(gpu_image) = images.get(&image.handle) else {296warn!("Unknown image for screenshot, skipping: {:?}", image);297continue;298};299let format = gpu_image.texture_format;300let (texture_view, state) = prepare_screenshot_state(301gpu_image.size,302format,303&render_device,304&screenshot_pipeline,305&pipeline_cache,306&mut pipelines,307);308prepared.insert(*entity, state);309view_target_attachments.insert(310target.clone(),311OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()),312);313}314NormalizedRenderTarget::TextureView(texture_view) => {315let Some(manual_texture_view) = manual_texture_views.get(texture_view) else {316warn!(317"Unknown manual texture view for screenshot, skipping: {:?}",318texture_view319);320continue;321};322let format = manual_texture_view.format;323let size = manual_texture_view.size.to_extents();324let (texture_view, state) = prepare_screenshot_state(325size,326format,327&render_device,328&screenshot_pipeline,329&pipeline_cache,330&mut pipelines,331);332prepared.insert(*entity, state);333view_target_attachments.insert(334target.clone(),335OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()),336);337}338NormalizedRenderTarget::None { .. } => {339// Nothing to screenshot!340}341}342}343}344345fn prepare_screenshot_state(346size: Extent3d,347format: TextureFormat,348render_device: &RenderDevice,349pipeline: &ScreenshotToScreenPipeline,350pipeline_cache: &PipelineCache,351pipelines: &mut SpecializedRenderPipelines<ScreenshotToScreenPipeline>,352) -> (TextureView, ScreenshotPreparedState) {353let texture = render_device.create_texture(&wgpu::TextureDescriptor {354label: Some("screenshot-capture-rendertarget"),355size,356mip_level_count: 1,357sample_count: 1,358dimension: wgpu::TextureDimension::D2,359format,360usage: TextureUsages::RENDER_ATTACHMENT361| TextureUsages::COPY_SRC362| TextureUsages::TEXTURE_BINDING,363view_formats: &[],364});365let texture_view = texture.create_view(&Default::default());366let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {367label: Some("screenshot-transfer-buffer"),368size: gpu_readback::get_aligned_size(size, format.pixel_size().unwrap_or(0) as u32) as u64,369usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,370mapped_at_creation: false,371});372let bind_group = render_device.create_bind_group(373"screenshot-to-screen-bind-group",374&pipeline.bind_group_layout,375&BindGroupEntries::single(&texture_view),376);377let pipeline_id = pipelines.specialize(pipeline_cache, pipeline, format);378379(380texture_view,381ScreenshotPreparedState {382texture,383buffer,384bind_group,385pipeline_id,386size,387},388)389}390391pub struct ScreenshotPlugin;392393impl Plugin for ScreenshotPlugin {394fn build(&self, app: &mut bevy_app::App) {395embedded_asset!(app, "screenshot.wgsl");396397let (tx, rx) = std::sync::mpsc::channel();398app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx))))399.add_systems(400First,401clear_screenshots402.after(event_update_system)403.before(ApplyDeferred),404)405.add_systems(Update, trigger_screenshots);406407let Some(render_app) = app.get_sub_app_mut(RenderApp) else {408return;409};410411render_app412.insert_resource(RenderScreenshotsSender(tx))413.init_resource::<RenderScreenshotTargets>()414.init_resource::<RenderScreenshotsPrepared>()415.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>()416.add_systems(RenderStartup, init_screenshot_to_screen_pipeline)417.add_systems(ExtractSchedule, extract_screenshots.ambiguous_with_all())418.add_systems(419Render,420prepare_screenshots421.after(prepare_view_attachments)422.before(prepare_view_targets)423.in_set(RenderSystems::ManageViews),424);425}426}427428#[derive(Resource)]429pub struct ScreenshotToScreenPipeline {430pub bind_group_layout: BindGroupLayout,431pub shader: Handle<Shader>,432}433434pub fn init_screenshot_to_screen_pipeline(435mut commands: Commands,436render_device: Res<RenderDevice>,437asset_server: Res<AssetServer>,438) {439let bind_group_layout = render_device.create_bind_group_layout(440"screenshot-to-screen-bgl",441&BindGroupLayoutEntries::single(442wgpu::ShaderStages::FRAGMENT,443texture_2d(wgpu::TextureSampleType::Float { filterable: false }),444),445);446447let shader = load_embedded_asset!(asset_server.as_ref(), "screenshot.wgsl");448449commands.insert_resource(ScreenshotToScreenPipeline {450bind_group_layout,451shader,452});453}454455impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {456type Key = TextureFormat;457458fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {459RenderPipelineDescriptor {460label: Some(Cow::Borrowed("screenshot-to-screen")),461layout: vec![self.bind_group_layout.clone()],462vertex: VertexState {463shader: self.shader.clone(),464..default()465},466primitive: wgpu::PrimitiveState {467cull_mode: Some(wgpu::Face::Back),468..Default::default()469},470multisample: Default::default(),471fragment: Some(FragmentState {472shader: self.shader.clone(),473targets: vec![Some(wgpu::ColorTargetState {474format: key,475blend: None,476write_mask: wgpu::ColorWrites::ALL,477})],478..default()479}),480..default()481}482}483}484485pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) {486let targets = world.resource::<RenderScreenshotTargets>();487let prepared = world.resource::<RenderScreenshotsPrepared>();488let pipelines = world.resource::<PipelineCache>();489let gpu_images = world.resource::<RenderAssets<GpuImage>>();490let windows = world.resource::<ExtractedWindows>();491let manual_texture_views = world.resource::<ManualTextureViews>();492493for (entity, render_target) in targets.iter() {494match render_target {495NormalizedRenderTarget::Window(window) => {496let window = window.entity();497let Some(window) = windows.get(&window) else {498continue;499};500let width = window.physical_width;501let height = window.physical_height;502let Some(texture_format) = window.swap_chain_texture_format else {503continue;504};505let Some(swap_chain_texture) = window.swap_chain_texture.as_ref() else {506continue;507};508let texture_view = swap_chain_texture.texture.create_view(&Default::default());509render_screenshot(510encoder,511prepared,512pipelines,513entity,514width,515height,516texture_format,517&texture_view,518);519}520NormalizedRenderTarget::Image(image) => {521let Some(gpu_image) = gpu_images.get(&image.handle) else {522warn!("Unknown image for screenshot, skipping: {:?}", image);523continue;524};525let width = gpu_image.size.width;526let height = gpu_image.size.height;527let texture_format = gpu_image.texture_format;528let texture_view = gpu_image.texture_view.deref();529render_screenshot(530encoder,531prepared,532pipelines,533entity,534width,535height,536texture_format,537texture_view,538);539}540NormalizedRenderTarget::TextureView(texture_view) => {541let Some(texture_view) = manual_texture_views.get(texture_view) else {542warn!(543"Unknown manual texture view for screenshot, skipping: {:?}",544texture_view545);546continue;547};548let width = texture_view.size.x;549let height = texture_view.size.y;550let texture_format = texture_view.format;551let texture_view = texture_view.texture_view.deref();552render_screenshot(553encoder,554prepared,555pipelines,556entity,557width,558height,559texture_format,560texture_view,561);562}563NormalizedRenderTarget::None { .. } => {564// Nothing to screenshot!565}566};567}568}569570fn render_screenshot(571encoder: &mut CommandEncoder,572prepared: &RenderScreenshotsPrepared,573pipelines: &PipelineCache,574entity: &Entity,575width: u32,576height: u32,577texture_format: TextureFormat,578texture_view: &wgpu::TextureView,579) {580if let Some(prepared_state) = &prepared.get(entity) {581let extent = Extent3d {582width,583height,584depth_or_array_layers: 1,585};586encoder.copy_texture_to_buffer(587prepared_state.texture.as_image_copy(),588wgpu::TexelCopyBufferInfo {589buffer: &prepared_state.buffer,590layout: gpu_readback::layout_data(extent, texture_format),591},592extent,593);594595if let Some(pipeline) = pipelines.get_render_pipeline(prepared_state.pipeline_id) {596let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {597label: Some("screenshot_to_screen_pass"),598color_attachments: &[Some(wgpu::RenderPassColorAttachment {599view: texture_view,600depth_slice: None,601resolve_target: None,602ops: wgpu::Operations {603load: wgpu::LoadOp::Load,604store: wgpu::StoreOp::Store,605},606})],607depth_stencil_attachment: None,608timestamp_writes: None,609occlusion_query_set: None,610});611pass.set_pipeline(pipeline);612pass.set_bind_group(0, &prepared_state.bind_group, &[]);613pass.draw(0..3, 0..1);614}615}616}617618pub(crate) fn collect_screenshots(world: &mut World) {619#[cfg(feature = "trace")]620let _span = tracing::info_span!("collect_screenshots").entered();621622let sender = world.resource::<RenderScreenshotsSender>().deref().clone();623let prepared = world.resource::<RenderScreenshotsPrepared>();624625for (entity, prepared) in prepared.iter() {626let entity = *entity;627let sender = sender.clone();628let width = prepared.size.width;629let height = prepared.size.height;630let texture_format = prepared.texture.format();631let Ok(pixel_size) = texture_format.pixel_size() else {632continue;633};634let buffer = prepared.buffer.clone();635636let finish = async move {637let (tx, rx) = async_channel::bounded(1);638let buffer_slice = buffer.slice(..);639// The polling for this map call is done every frame when the command queue is submitted.640buffer_slice.map_async(wgpu::MapMode::Read, move |result| {641let err = result.err();642if err.is_some() {643panic!("{}", err.unwrap().to_string());644}645tx.try_send(()).unwrap();646});647rx.recv().await.unwrap();648let data = buffer_slice.get_mapped_range();649// we immediately move the data to CPU memory to avoid holding the mapped view for long650let mut result = Vec::from(&*data);651drop(data);652653if result.len() != ((width * height) as usize * pixel_size) {654// Our buffer has been padded because we needed to align to a multiple of 256.655// We remove this padding here656let initial_row_bytes = width as usize * pixel_size;657let buffered_row_bytes =658gpu_readback::align_byte_size(width * pixel_size as u32) as usize;659660let mut take_offset = buffered_row_bytes;661let mut place_offset = initial_row_bytes;662for _ in 1..height {663result.copy_within(take_offset..take_offset + buffered_row_bytes, place_offset);664take_offset += buffered_row_bytes;665place_offset += initial_row_bytes;666}667result.truncate(initial_row_bytes * height as usize);668}669670if let Err(e) = sender.send((671entity,672Image::new(673Extent3d {674width,675height,676depth_or_array_layers: 1,677},678wgpu::TextureDimension::D2,679result,680texture_format,681RenderAssetUsages::RENDER_WORLD,682),683)) {684error!("Failed to send screenshot: {}", e);685}686};687688AsyncComputeTaskPool::get().spawn(finish).detach();689}690}691692693