From 060eab94eaeac0ff861d6b7d78b7cf023162f7fd Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Wed, 17 Apr 2024 14:32:44 +0300 Subject: [PATCH 01/14] Headless renderer example has been added --- Cargo.toml | 12 + examples/app/headless_renderer.rs | 444 ++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 examples/app/headless_renderer.rs diff --git a/Cargo.toml b/Cargo.toml index 391cd213472b4..779833cafa71d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -343,6 +343,7 @@ bytemuck = "1.7" # Needed to poll Task examples futures-lite = "2.0.1" crossbeam-channel = "0.5.0" +async-channel = "2.2.1" argh = "0.1.12" thiserror = "1.0" @@ -1217,6 +1218,17 @@ description = "An application that runs with default plugins and displays an emp category = "Application" wasm = false +[[example]] +name = "headless_renderer" +path = "examples/app/headless_renderer.rs" +doc-scrape-examples = true + +[package.metadata.example.headless_renderer] +name = "Headless Renderer" +description = "An application that runs with no window, but renders into image file" +category = "Application" +wasm = false + [[example]] name = "without_winit" path = "examples/app/without_winit.rs" diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs new file mode 100644 index 0000000000000..3d191d4e5d00e --- /dev/null +++ b/examples/app/headless_renderer.rs @@ -0,0 +1,444 @@ +//! This example illustrates how to make headless renderer + +mod frame_capture { + pub mod image_copy { + use bevy::prelude::*; + use bevy::render::{ + render_asset::RenderAssets, + render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext}, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, + ImageCopyBuffer, ImageDataLayout, Maintain, MapMode, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + Extract, RenderApp, + }; + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }; + + pub fn receive_images( + image_copiers: Query<&ImageCopier>, + mut images: ResMut>, + render_device: Res, + ) { + for image_copier in image_copiers.iter() { + if !image_copier.enabled() { + continue; + } + // Derived from: https://sotrh.github.io/learn-wgpu/showcase/windowless/#a-triangle-without-a-window + // We need to scope the mapping variables so that we can + // unmap the buffer + futures_lite::future::block_on(async { + let buffer_slice = image_copier.buffer.slice(..); + + // NOTE: We have to create the mapping THEN device.poll() before await + // the future. Otherwise the application will freeze. + let (tx, rx) = async_channel::bounded(1); + buffer_slice.map_async(MapMode::Read, move |result| { + tx.send_blocking(result).unwrap(); + }); + render_device.poll(Maintain::Wait); + rx.recv().await.unwrap().unwrap(); + if let Some(image) = images.get_mut(&image_copier.dst_image) { + image.data = buffer_slice.get_mapped_range().to_vec(); + } + + image_copier.buffer.unmap(); + }); + } + } + + #[derive(Debug, PartialEq, Eq, Clone, Hash, bevy::render::render_graph::RenderLabel)] + pub struct ImageCopy; + + pub struct ImageCopyPlugin; + impl Plugin for ImageCopyPlugin { + fn build(&self, app: &mut App) { + let render_app = app + .add_systems(Update, receive_images) + .sub_app_mut(RenderApp); + + render_app.add_systems(ExtractSchedule, image_copy_extract); + + let mut graph = render_app + .world_mut() + .get_resource_mut::() + .unwrap(); + + graph.add_node(ImageCopy, ImageCopyDriver); + + graph.add_node_edge(ImageCopy, bevy::render::graph::CameraDriverLabel); + } + } + + #[derive(Clone, Default, Resource, Deref, DerefMut)] + pub struct ImageCopiers(pub Vec); + + #[derive(Clone, Component)] + pub struct ImageCopier { + buffer: Buffer, + enabled: Arc, + src_image: Handle, + dst_image: Handle, + } + + impl ImageCopier { + pub fn new( + src_image: Handle, + dst_image: Handle, + size: Extent3d, + render_device: &RenderDevice, + ) -> ImageCopier { + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row((size.width) as usize) * 4; + + let cpu_buffer = render_device.create_buffer(&BufferDescriptor { + label: None, + size: padded_bytes_per_row as u64 * size.height as u64, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + ImageCopier { + buffer: cpu_buffer, + src_image, + dst_image, + enabled: Arc::new(AtomicBool::new(true)), + } + } + + pub fn enabled(&self) -> bool { + self.enabled.load(Ordering::Relaxed) + } + } + + pub fn image_copy_extract( + mut commands: Commands, + image_copiers: Extract>, + ) { + commands.insert_resource(ImageCopiers( + image_copiers.iter().cloned().collect::>(), + )); + } + + #[derive(Default)] + pub struct ImageCopyDriver; + + impl render_graph::Node for ImageCopyDriver { + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let image_copiers = world.get_resource::().unwrap(); + let gpu_images = world + .get_resource::>() + .unwrap(); + + for image_copier in image_copiers.iter() { + if !image_copier.enabled() { + continue; + } + + let src_image = gpu_images.get(&image_copier.src_image).unwrap(); + + let mut encoder = render_context + .render_device() + .create_command_encoder(&CommandEncoderDescriptor::default()); + + let block_dimensions = src_image.texture_format.block_dimensions(); + let block_size = src_image.texture_format.block_copy_size(None).unwrap(); + + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( + (src_image.size.x as usize / block_dimensions.0 as usize) + * block_size as usize, + ); + + let texture_extent = Extent3d { + width: src_image.size.x as u32, + height: src_image.size.y as u32, + depth_or_array_layers: 1, + }; + + encoder.copy_texture_to_buffer( + src_image.texture.as_image_copy(), + ImageCopyBuffer { + buffer: &image_copier.buffer, + layout: ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new(padded_bytes_per_row as u32) + .unwrap() + .into(), + ), + rows_per_image: None, + }, + }, + texture_extent, + ); + + let render_queue = world.get_resource::().unwrap(); + render_queue.submit(std::iter::once(encoder.finish())); + } + + Ok(()) + } + } + } + pub mod scene { + use std::path::PathBuf; + + use bevy::{ + app::AppExit, + prelude::*, + render::{ + camera::RenderTarget, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + }, + }; + + use super::image_copy::ImageCopier; + + #[derive(Component, Default)] + pub struct CaptureCamera; + + #[derive(Component, Deref, DerefMut)] + struct ImageToSave(Handle); + + pub struct CaptureFramePlugin; + impl Plugin for CaptureFramePlugin { + fn build(&self, app: &mut App) { + println!("Adding CaptureFramePlugin"); + app.add_systems(PostUpdate, update); + } + } + + #[derive(Debug, Default, Resource, Event)] + pub struct SceneController { + state: SceneState, + name: String, + width: u32, + height: u32, + single_image: bool, + } + + impl SceneController { + pub fn new(width: u32, height: u32, single_image: bool) -> SceneController { + SceneController { + state: SceneState::BuildScene, + name: String::from(""), + width, + height, + single_image, + } + } + } + + #[derive(Debug, Default)] + pub enum SceneState { + #[default] + BuildScene, + Render(u32), + } + + pub fn setup_render_target( + commands: &mut Commands, + images: &mut ResMut>, + render_device: &Res, + scene_controller: &mut ResMut, + pre_roll_frames: u32, + scene_name: String, + ) -> RenderTarget { + let size = Extent3d { + width: scene_controller.width, + height: scene_controller.height, + ..Default::default() + }; + + // This is the texture that will be rendered to. + let mut render_target_image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::COPY_SRC + | TextureUsages::COPY_DST + | TextureUsages::TEXTURE_BINDING + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ..Default::default() + }; + render_target_image.resize(size); + let render_target_image_handle = images.add(render_target_image); + + // This is the texture that will be copied to. + let mut cpu_image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ..Default::default() + }; + cpu_image.resize(size); + let cpu_image_handle = images.add(cpu_image); + + commands.spawn(ImageCopier::new( + render_target_image_handle.clone(), + cpu_image_handle.clone(), + size, + render_device, + )); + + commands.spawn(ImageToSave(cpu_image_handle)); + + scene_controller.state = SceneState::Render(pre_roll_frames); + scene_controller.name = scene_name; + RenderTarget::Image(render_target_image_handle) + } + + fn update( + images_to_save: Query<&ImageToSave>, + mut images: ResMut>, + mut scene_controller: ResMut, + mut app_exit_writer: EventWriter, + ) { + if let SceneState::Render(n) = scene_controller.state { + if n < 1 { + use rand::Rng; + let mut rng = rand::thread_rng(); + for image in images_to_save.iter() { + let img_bytes = images.get_mut(image.id()).unwrap(); + + let img = match img_bytes.clone().try_into_dynamic() { + Ok(img) => img.to_rgba8(), + Err(e) => panic!("Failed to create image buffer {e:?}"), + }; + + let images_dir = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); + print!("Saving image to: {:?}\n", images_dir); + std::fs::create_dir_all(&images_dir).unwrap(); + + let number = rng.gen::(); + let image_path = images_dir.join(format!("{number:032X}.png")); + if let Err(e) = img.save(image_path) { + panic!("Failed to save image: {}", e); + }; + } + if scene_controller.single_image { + app_exit_writer.send(AppExit); + } + } else { + scene_controller.state = SceneState::Render(n - 1); + } + } + } + } +} + +struct AppConfig { + width: u32, + height: u32, + single_image: bool, +} + +use bevy::{ + app::ScheduleRunnerPlugin, core_pipeline::tonemapping::Tonemapping, prelude::*, + render::renderer::RenderDevice, +}; + +fn main() { + let mut app = App::new(); + + let config = AppConfig { + width: 1920, + height: 1080, + single_image: true, + }; + + // setup frame capture + app.insert_resource(frame_capture::scene::SceneController::new( + config.width, + config.height, + config.single_image, + )); + app.insert_resource(ClearColor(Color::srgb_u8(0, 0, 0))); + + app.add_plugins( + DefaultPlugins + .set(ImagePlugin::default_nearest()) + .set(WindowPlugin { + primary_window: None, + exit_condition: bevy::window::ExitCondition::DontExit, + close_when_requested: false, + }) + .build() + // avoid panic, caused by using buffer by main world and render world at same time: + // thread '' panicked at /path/to/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wgpu-0.19.3/src/backend/wgpu_core.rs:2225:30: + // Error in Queue::submit: Validation Error + // + // Caused by: + // Buffer Id(0,1,your_backend_type) is still mapped + .disable::(), + ); + + app.add_plugins(frame_capture::image_copy::ImageCopyPlugin); + + // headless frame capture + app.add_plugins(frame_capture::scene::CaptureFramePlugin); + + app.add_plugins(ScheduleRunnerPlugin::run_loop( + std::time::Duration::from_secs_f64(1.0 / 60.0), + )); + + app.init_resource::(); + app.add_event::(); + + app.add_systems(Startup, setup); + + app.run(); +} + +fn setup( + mut commands: Commands, + mut images: ResMut>, + mut scene_controller: ResMut, + render_device: Res, +) { + let render_target = frame_capture::scene::setup_render_target( + &mut commands, + &mut images, + &render_device, + &mut scene_controller, + 15, + String::from("main_scene"), + ); + + // Scene is empty, but you can add any mesh to generate non black box picture + + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 6., 12.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + tonemapping: Tonemapping::None, + camera: Camera { + target: render_target, + ..default() + }, + ..default() + }); +} From 5a2f443355730eb620c8c4e7b39c9956468423ce Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Wed, 17 Apr 2024 14:51:43 +0300 Subject: [PATCH 02/14] Update examples list --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index ce676d23116d5..1e9e929a5f85a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -182,6 +182,7 @@ Example | Description [Empty](../examples/app/empty.rs) | An empty application (does nothing) [Empty with Defaults](../examples/app/empty_defaults.rs) | An empty application with default plugins [Headless](../examples/app/headless.rs) | An application that runs without default plugins +[Headless Renderer](../examples/app/headless_renderer.rs) | An application that runs with no window, but renders into image file [Log layers](../examples/app/log_layers.rs) | Illustrate how to add custom log layers [Logs](../examples/app/logs.rs) | Illustrate how to use generate log output [No Renderer](../examples/app/no_renderer.rs) | An application that runs with default plugins and displays an empty window, but without an actual renderer From 5fcba485d7b6bf734c474199314d155069146a14 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Wed, 17 Apr 2024 15:16:26 +0300 Subject: [PATCH 03/14] Fix warnings --- examples/app/headless_renderer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 3d191d4e5d00e..ec3dc764140dd 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -158,8 +158,8 @@ mod frame_capture { ); let texture_extent = Extent3d { - width: src_image.size.x as u32, - height: src_image.size.y as u32, + width: src_image.size.x, + height: src_image.size.y, depth_or_array_layers: 1, }; @@ -332,7 +332,7 @@ mod frame_capture { let images_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); - print!("Saving image to: {:?}\n", images_dir); + println!("Saving image to: {images_dir:?}"); std::fs::create_dir_all(&images_dir).unwrap(); let number = rng.gen::(); From 40bb6d8c809f9147970f6cfebb05ee6767a5574b Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Wed, 24 Apr 2024 13:27:39 +0300 Subject: [PATCH 04/14] Fixes based on review feedback and according to gpu_readback example --- Cargo.toml | 1 - examples/app/headless_renderer.rs | 374 +++++++++++++++++------------- 2 files changed, 218 insertions(+), 157 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 779833cafa71d..22b3c065fc764 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -343,7 +343,6 @@ bytemuck = "1.7" # Needed to poll Task examples futures-lite = "2.0.1" crossbeam-channel = "0.5.0" -async-channel = "2.2.1" argh = "0.1.12" thiserror = "1.0" diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index ec3dc764140dd..c454f6c3a40d6 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -1,75 +1,222 @@ //! This example illustrates how to make headless renderer +//! derived from: https://sotrh.github.io/learn-wgpu/showcase/windowless/#a-triangle-without-a-window +//! It follows this steps: +//! 1) Render from camera to gpu-image render target +//! 2) Copy form gpu image to buffer using ImageCopyDriver node in RenderGraph +//! 3) Copy from buffer to channel using image_copy::receive_image_from_buffer after RenderSet::Render +//! 4) Save from channel to random named file using scene::update at PostUpdate in MainWorld +//! 5) Exit if single_image setting is set + +use bevy::{ + app::ScheduleRunnerPlugin, core_pipeline::tonemapping::Tonemapping, prelude::*, + render::renderer::RenderDevice, +}; + +use crossbeam_channel::{Receiver, Sender}; + +// To communicate between the main world and the render world we need a channel. +// Since the main world and render world run in parallel, there will always be a frame of latency +// between the data sent from the render world and the data received in the main world +// +// frame n => render world sends data through the channel at the end of the frame +// frame n + 1 => main world receives the data +// +// Receiver and Sender are kept in resources because there is single camera and single target +// That's why there is single images role, if you want to differentiate images +// from different cameras, you should keep Receiver in ImageCopier and Sender in ImageToSave +// or send some id with data + +/// This will receive asynchronously any data sent from the render world +#[derive(Resource, Deref)] +struct MainWorldReceiver(Receiver>); + +/// This will send asynchronously any data to the main world +#[derive(Resource, Deref)] +struct RenderWorldSender(Sender>); + +// Parameters of resulting image +struct AppConfig { + width: u32, + height: u32, + single_image: bool, +} + +fn main() { + let config = AppConfig { + width: 1920, + height: 1080, + single_image: true, + }; + + // setup frame capture + App::new() + .insert_resource(frame_capture::scene::SceneController::new( + config.width, + config.height, + config.single_image, + )) + .insert_resource(ClearColor(Color::srgb_u8(0, 0, 0))) + .add_plugins( + DefaultPlugins + .set(ImagePlugin::default_nearest()) + .set(WindowPlugin { + primary_window: None, + exit_condition: bevy::window::ExitCondition::DontExit, + close_when_requested: false, + }), + ) + .add_plugins(frame_capture::image_copy::ImageCopyPlugin) + // headless frame capture + .add_plugins(frame_capture::scene::CaptureFramePlugin) + .add_plugins(ScheduleRunnerPlugin::run_loop( + std::time::Duration::from_secs_f64(1.0 / 60.0), + )) + .init_resource::() + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut images: ResMut>, + mut scene_controller: ResMut, + render_device: Res, +) { + let render_target = frame_capture::scene::setup_render_target( + &mut commands, + &mut images, + &render_device, + &mut scene_controller, + 15, + String::from("main_scene"), + ); + + // Scene is empty, but you can add any mesh to generate non black box picture + + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 6., 12.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + tonemapping: Tonemapping::None, + camera: Camera { + target: render_target, + ..default() + }, + ..default() + }); +} mod frame_capture { pub mod image_copy { + use crate::{MainWorldReceiver, RenderWorldSender}; use bevy::prelude::*; use bevy::render::{ render_asset::RenderAssets, - render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext}, + render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel}, render_resource::{ Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer, ImageDataLayout, Maintain, MapMode, }, renderer::{RenderContext, RenderDevice, RenderQueue}, - Extract, RenderApp, + Extract, Render, RenderApp, RenderSet, }; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; - pub fn receive_images( - image_copiers: Query<&ImageCopier>, - mut images: ResMut>, + pub fn receive_image_from_buffer( + image_copiers: Res, render_device: Res, + sender: Res, ) { - for image_copier in image_copiers.iter() { + for image_copier in image_copiers.0.iter() { if !image_copier.enabled() { continue; } - // Derived from: https://sotrh.github.io/learn-wgpu/showcase/windowless/#a-triangle-without-a-window - // We need to scope the mapping variables so that we can - // unmap the buffer - futures_lite::future::block_on(async { - let buffer_slice = image_copier.buffer.slice(..); - - // NOTE: We have to create the mapping THEN device.poll() before await - // the future. Otherwise the application will freeze. - let (tx, rx) = async_channel::bounded(1); - buffer_slice.map_async(MapMode::Read, move |result| { - tx.send_blocking(result).unwrap(); - }); - render_device.poll(Maintain::Wait); - rx.recv().await.unwrap().unwrap(); - if let Some(image) = images.get_mut(&image_copier.dst_image) { - image.data = buffer_slice.get_mapped_range().to_vec(); - } - image_copier.buffer.unmap(); + // Finally time to get our data back from the gpu. + // First we get a buffer slice which represents a chunk of the buffer (which we + // can't access yet). + // We want the whole thing so use unbounded range. + let buffer_slice = image_copier.buffer.slice(..); + + // Now things get complicated. WebGPU, for safety reasons, only allows either the GPU + // or CPU to access a buffer's contents at a time. We need to "map" the buffer which means + // flipping ownership of the buffer over to the CPU and making access legal. We do this + // with `BufferSlice::map_async`. + // + // The problem is that map_async is not an async function so we can't await it. What + // we need to do instead is pass in a closure that will be executed when the slice is + // either mapped or the mapping has failed. + // + // The problem with this is that we don't have a reliable way to wait in the main + // code for the buffer to be mapped and even worse, calling get_mapped_range or + // get_mapped_range_mut prematurely will cause a panic, not return an error. + // + // Using channels solves this as awaiting the receiving of a message from + // the passed closure will force the outside code to wait. It also doesn't hurt + // if the closure finishes before the outside code catches up as the message is + // buffered and receiving will just pick that up. + // + // It may also be worth noting that although on native, the usage of asynchronous + // channels is wholly unnecessary, for the sake of portability to WASM + // we'll use async channels that work on both native and WASM. + + let (s, r) = crossbeam_channel::bounded(1); + + // Maps the buffer so it can be read on the cpu + buffer_slice.map_async(MapMode::Read, move |r| match r { + // This will execute once the gpu is ready, so after the call to poll() + Ok(r) => s.send(r).expect("Failed to send map update"), + Err(err) => panic!("Failed to map buffer {err}"), }); + + // In order for the mapping to be completed, one of three things must happen. + // One of those can be calling `Device::poll`. This isn't necessary on the web as devices + // are polled automatically but natively, we need to make sure this happens manually. + // `Maintain::Wait` will cause the thread to wait on native but not on WebGpu. + + // This blocks until the gpu is done executing everything + render_device.poll(Maintain::wait()).panic_on_timeout(); + + // This blocks until the buffer is mapped + r.recv().expect("Failed to receive the map_async message"); + + // This could fail on app exit, if Main world clears resources while Render world still renders + sender + .send(buffer_slice.get_mapped_range().to_vec()) + .expect("Failed to send data to main world"); + + // We need to make sure all `BufferView`'s are dropped before we do what we're about + // to do. + // Unmap so that we can copy to the staging buffer in the next iteration. + image_copier.buffer.unmap(); } } - #[derive(Debug, PartialEq, Eq, Clone, Hash, bevy::render::render_graph::RenderLabel)] + #[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)] pub struct ImageCopy; + // Plugin for Render world part of work pub struct ImageCopyPlugin; impl Plugin for ImageCopyPlugin { fn build(&self, app: &mut App) { + let (s, r) = crossbeam_channel::unbounded(); + let render_app = app - .add_systems(Update, receive_images) + .insert_resource(MainWorldReceiver(r)) .sub_app_mut(RenderApp); - render_app.add_systems(ExtractSchedule, image_copy_extract); - - let mut graph = render_app - .world_mut() - .get_resource_mut::() - .unwrap(); - + let mut graph = render_app.world_mut().resource_mut::(); graph.add_node(ImageCopy, ImageCopyDriver); - - graph.add_node_edge(ImageCopy, bevy::render::graph::CameraDriverLabel); + graph.add_node_edge(bevy::render::graph::CameraDriverLabel, ImageCopy); + + render_app + .insert_resource(RenderWorldSender(s)) + // Make ImageCopiers accessible in RenderWorld system and plugin + .add_systems(ExtractSchedule, image_copy_extract) + // Receives image data from buffer to channel + // so we need to run it after the render graph is done + .add_systems(Render, receive_image_from_buffer.after(RenderSet::Render)); } } @@ -81,13 +228,11 @@ mod frame_capture { buffer: Buffer, enabled: Arc, src_image: Handle, - dst_image: Handle, } impl ImageCopier { pub fn new( src_image: Handle, - dst_image: Handle, size: Extent3d, render_device: &RenderDevice, ) -> ImageCopier { @@ -104,7 +249,6 @@ mod frame_capture { ImageCopier { buffer: cpu_buffer, src_image, - dst_image, enabled: Arc::new(AtomicBool::new(true)), } } @@ -126,6 +270,7 @@ mod frame_capture { #[derive(Default)] pub struct ImageCopyDriver; + // Copies image content from render target to buffer impl render_graph::Node for ImageCopyDriver { fn run( &self, @@ -189,8 +334,7 @@ mod frame_capture { } } pub mod scene { - use std::path::PathBuf; - + use super::{super::MainWorldReceiver, image_copy::ImageCopier}; use bevy::{ app::AppExit, prelude::*, @@ -202,8 +346,7 @@ mod frame_capture { renderer::RenderDevice, }, }; - - use super::image_copy::ImageCopier; + use std::path::PathBuf; #[derive(Component, Default)] pub struct CaptureCamera; @@ -219,7 +362,7 @@ mod frame_capture { } } - #[derive(Debug, Default, Resource, Event)] + #[derive(Debug, Default, Resource)] pub struct SceneController { state: SceneState, name: String, @@ -300,7 +443,6 @@ mod frame_capture { commands.spawn(ImageCopier::new( render_target_image_handle.clone(), - cpu_image_handle.clone(), size, render_device, )); @@ -312,8 +454,10 @@ mod frame_capture { RenderTarget::Image(render_target_image_handle) } + // Takes from channel image content sent from render world and saves it to disk fn update( images_to_save: Query<&ImageToSave>, + receiver: Res, mut images: ResMut>, mut scene_controller: ResMut, mut app_exit_writer: EventWriter, @@ -322,123 +466,41 @@ mod frame_capture { if n < 1 { use rand::Rng; let mut rng = rand::thread_rng(); - for image in images_to_save.iter() { - let img_bytes = images.get_mut(image.id()).unwrap(); - - let img = match img_bytes.clone().try_into_dynamic() { - Ok(img) => img.to_rgba8(), - Err(e) => panic!("Failed to create image buffer {e:?}"), - }; - - let images_dir = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); - println!("Saving image to: {images_dir:?}"); - std::fs::create_dir_all(&images_dir).unwrap(); - - let number = rng.gen::(); - let image_path = images_dir.join(format!("{number:032X}.png")); - if let Err(e) = img.save(image_path) { - panic!("Failed to save image: {}", e); - }; - } - if scene_controller.single_image { - app_exit_writer.send(AppExit); + + // We don't want to block the main world on this, + // so we use try_recv which attempts to receive without blocking + while let Ok(data) = receiver.try_recv() { + for image in images_to_save.iter() { + let img_bytes = images.get_mut(image.id()).unwrap(); + img_bytes.data = data.clone(); + + let img = match img_bytes.clone().try_into_dynamic() { + Ok(img) => img.to_rgba8(), + Err(e) => panic!("Failed to create image buffer {e:?}"), + }; + + let images_dir = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); + println!("Saving image to: {images_dir:?}"); + std::fs::create_dir_all(&images_dir).unwrap(); + + let number = rng.gen::(); + let image_path = images_dir.join(format!("{number:032X}.png")); + if let Err(e) = img.save(image_path) { + panic!("Failed to save image: {}", e); + }; + } + if scene_controller.single_image { + app_exit_writer.send(AppExit); + break; + } } } else { + // clears channel for skipped frames + while let Ok(_) = receiver.try_recv() {} scene_controller.state = SceneState::Render(n - 1); } } } } } - -struct AppConfig { - width: u32, - height: u32, - single_image: bool, -} - -use bevy::{ - app::ScheduleRunnerPlugin, core_pipeline::tonemapping::Tonemapping, prelude::*, - render::renderer::RenderDevice, -}; - -fn main() { - let mut app = App::new(); - - let config = AppConfig { - width: 1920, - height: 1080, - single_image: true, - }; - - // setup frame capture - app.insert_resource(frame_capture::scene::SceneController::new( - config.width, - config.height, - config.single_image, - )); - app.insert_resource(ClearColor(Color::srgb_u8(0, 0, 0))); - - app.add_plugins( - DefaultPlugins - .set(ImagePlugin::default_nearest()) - .set(WindowPlugin { - primary_window: None, - exit_condition: bevy::window::ExitCondition::DontExit, - close_when_requested: false, - }) - .build() - // avoid panic, caused by using buffer by main world and render world at same time: - // thread '' panicked at /path/to/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wgpu-0.19.3/src/backend/wgpu_core.rs:2225:30: - // Error in Queue::submit: Validation Error - // - // Caused by: - // Buffer Id(0,1,your_backend_type) is still mapped - .disable::(), - ); - - app.add_plugins(frame_capture::image_copy::ImageCopyPlugin); - - // headless frame capture - app.add_plugins(frame_capture::scene::CaptureFramePlugin); - - app.add_plugins(ScheduleRunnerPlugin::run_loop( - std::time::Duration::from_secs_f64(1.0 / 60.0), - )); - - app.init_resource::(); - app.add_event::(); - - app.add_systems(Startup, setup); - - app.run(); -} - -fn setup( - mut commands: Commands, - mut images: ResMut>, - mut scene_controller: ResMut, - render_device: Res, -) { - let render_target = frame_capture::scene::setup_render_target( - &mut commands, - &mut images, - &render_device, - &mut scene_controller, - 15, - String::from("main_scene"), - ); - - // Scene is empty, but you can add any mesh to generate non black box picture - - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0.0, 6., 12.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), - tonemapping: Tonemapping::None, - camera: Camera { - target: render_target, - ..default() - }, - ..default() - }); -} From d85d6a744b443a2d3cf15b1f2ff7ce7ff22e55ea Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Thu, 25 Apr 2024 11:25:52 +0300 Subject: [PATCH 05/14] Fix ci build error --- examples/app/headless_renderer.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index c454f6c3a40d6..ce0b9de0d8e86 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -1,11 +1,11 @@ //! This example illustrates how to make headless renderer -//! derived from: https://sotrh.github.io/learn-wgpu/showcase/windowless/#a-triangle-without-a-window +//! derived from: //! It follows this steps: //! 1) Render from camera to gpu-image render target -//! 2) Copy form gpu image to buffer using ImageCopyDriver node in RenderGraph -//! 3) Copy from buffer to channel using image_copy::receive_image_from_buffer after RenderSet::Render -//! 4) Save from channel to random named file using scene::update at PostUpdate in MainWorld -//! 5) Exit if single_image setting is set +//! 2) Copy form gpu image to buffer using `ImageCopyDriver` node in `RenderGraph` +//! 3) Copy from buffer to channel using `image_copy::receive_image_from_buffer` after `RenderSet::Render` +//! 4) Save from channel to random named file using `scene::update` at `PostUpdate` in `MainWorld` +//! 5) Exit if `single_image` setting is set use bevy::{ app::ScheduleRunnerPlugin, core_pipeline::tonemapping::Tonemapping, prelude::*, @@ -491,13 +491,13 @@ mod frame_capture { }; } if scene_controller.single_image { - app_exit_writer.send(AppExit); + app_exit_writer.send(AppExit::Success); break; } } } else { // clears channel for skipped frames - while let Ok(_) = receiver.try_recv() {} + while receiver.try_recv().is_ok() {} scene_controller.state = SceneState::Render(n - 1); } } From 19e16909c7757da651077bed33e9bb1dced76dc5 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Sat, 27 Apr 2024 12:37:06 +0300 Subject: [PATCH 06/14] Apply suggestions from code review Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com> --- examples/app/headless_renderer.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index ce0b9de0d8e86..16ad24d348e1f 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -1,11 +1,11 @@ //! This example illustrates how to make headless renderer //! derived from: //! It follows this steps: -//! 1) Render from camera to gpu-image render target -//! 2) Copy form gpu image to buffer using `ImageCopyDriver` node in `RenderGraph` -//! 3) Copy from buffer to channel using `image_copy::receive_image_from_buffer` after `RenderSet::Render` -//! 4) Save from channel to random named file using `scene::update` at `PostUpdate` in `MainWorld` -//! 5) Exit if `single_image` setting is set +//! 1. Render from camera to gpu-image render target +//! 2. Copy form gpu image to buffer using `ImageCopyDriver` node in `RenderGraph` +//! 3. Copy from buffer to channel using `image_copy::receive_image_from_buffer` after `RenderSet::Render` +//! 4. Save from channel to random named file using `scene::update` at `PostUpdate` in `MainWorld` +//! 5. Exit if `single_image` setting is set use bevy::{ app::ScheduleRunnerPlugin, core_pipeline::tonemapping::Tonemapping, prelude::*, @@ -59,6 +59,7 @@ fn main() { .add_plugins( DefaultPlugins .set(ImagePlugin::default_nearest()) + // Do not create a window on startup. .set(WindowPlugin { primary_window: None, exit_condition: bevy::window::ExitCondition::DontExit, @@ -69,6 +70,7 @@ fn main() { // headless frame capture .add_plugins(frame_capture::scene::CaptureFramePlugin) .add_plugins(ScheduleRunnerPlugin::run_loop( + // Run 60 times per second. std::time::Duration::from_secs_f64(1.0 / 60.0), )) .init_resource::() @@ -88,7 +90,7 @@ fn setup( &render_device, &mut scene_controller, 15, - String::from("main_scene"), + "main_scene".into(), ); // Scene is empty, but you can add any mesh to generate non black box picture From 1bd7cbab926f4a4a71eaa78a5d6436712fab9f19 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Mon, 29 Apr 2024 12:19:33 +0300 Subject: [PATCH 07/14] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit replace println to info Co-authored-by: François Mockers --- examples/app/headless_renderer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 16ad24d348e1f..e7663a564e32f 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -359,7 +359,7 @@ mod frame_capture { pub struct CaptureFramePlugin; impl Plugin for CaptureFramePlugin { fn build(&self, app: &mut App) { - println!("Adding CaptureFramePlugin"); + info!("Adding CaptureFramePlugin"); app.add_systems(PostUpdate, update); } } @@ -483,7 +483,7 @@ mod frame_capture { let images_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); - println!("Saving image to: {images_dir:?}"); + info!("Saving image to: {images_dir:?}"); std::fs::create_dir_all(&images_dir).unwrap(); let number = rng.gen::(); From 0475ec96e5c3bb6ec536090d31b6e5a94e825988 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Mon, 29 Apr 2024 14:15:48 +0300 Subject: [PATCH 08/14] Fixed panic on app exit --- examples/app/headless_renderer.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index e7663a564e32f..b4e32e2f61e9a 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -183,10 +183,8 @@ mod frame_capture { // This blocks until the buffer is mapped r.recv().expect("Failed to receive the map_async message"); - // This could fail on app exit, if Main world clears resources while Render world still renders - sender - .send(buffer_slice.get_mapped_range().to_vec()) - .expect("Failed to send data to main world"); + // This could fail on app exit, if Main world clears resources (including receiver) while Render world still renders + let _ = sender.send(buffer_slice.get_mapped_range().to_vec()); // We need to make sure all `BufferView`'s are dropped before we do what we're about // to do. From e5d805aad119e00a6cfef98f3502f6394c0c79d5 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Mon, 29 Apr 2024 17:55:08 +0300 Subject: [PATCH 09/14] Filled scene to generate nonblack image --- examples/app/headless_renderer.rs | 50 +++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index b4e32e2f61e9a..7e97d51acab47 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -80,6 +80,8 @@ fn main() { fn setup( mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, mut images: ResMut>, mut scene_controller: ResMut, render_device: Res, @@ -89,16 +91,49 @@ fn setup( &mut images, &render_device, &mut scene_controller, - 15, + // pre_roll_frames should be big enought for full scene render, + // but the bigger it is, the longer example will run. + // To visualize stages of scene rendering change this param to 0 + // and change AppConfig::single_image to false in main + // Stages are: + // 1. Transparent image + // 2. Few black box images + // 3. Fully rendered scene images + // Exact number depends on device speed, device load and scene size + 30, "main_scene".into(), ); - // Scene is empty, but you can add any mesh to generate non black box picture + // Scene example for non black box picture + // circular base + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(4.0)), + material: materials.add(Color::WHITE), + transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + ..default() + }); + // cube + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), + material: materials.add(Color::srgb_u8(124, 144, 255)), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0.0, 6., 12.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), tonemapping: Tonemapping::None, camera: Camera { + // render to image target: render_target, ..default() }, @@ -469,10 +504,16 @@ mod frame_capture { // We don't want to block the main world on this, // so we use try_recv which attempts to receive without blocking + let mut image_data = Vec::new(); while let Ok(data) = receiver.try_recv() { + // image generation could be faster than saving to fs, + // that's why use only last of them + image_data = data; + } + if !image_data.is_empty() { for image in images_to_save.iter() { let img_bytes = images.get_mut(image.id()).unwrap(); - img_bytes.data = data.clone(); + img_bytes.data = image_data.clone(); let img = match img_bytes.clone().try_into_dynamic() { Ok(img) => img.to_rgba8(), @@ -492,7 +533,6 @@ mod frame_capture { } if scene_controller.single_image { app_exit_writer.send(AppExit::Success); - break; } } } else { From 946ae14ae726838855f97abefa058d662079d442 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Mon, 29 Apr 2024 18:40:04 +0300 Subject: [PATCH 10/14] Using incrementing numbers instead of random values as filenames --- examples/app/headless_renderer.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 7e97d51acab47..84819331b4fe8 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -91,7 +91,7 @@ fn setup( &mut images, &render_device, &mut scene_controller, - // pre_roll_frames should be big enought for full scene render, + // pre_roll_frames should be big enough for full scene render, // but the bigger it is, the longer example will run. // To visualize stages of scene rendering change this param to 0 // and change AppConfig::single_image to false in main @@ -100,7 +100,7 @@ fn setup( // 2. Few black box images // 3. Fully rendered scene images // Exact number depends on device speed, device load and scene size - 30, + 40, "main_scene".into(), ); @@ -499,9 +499,6 @@ mod frame_capture { ) { if let SceneState::Render(n) = scene_controller.state { if n < 1 { - use rand::Rng; - let mut rng = rand::thread_rng(); - // We don't want to block the main world on this, // so we use try_recv which attempts to receive without blocking let mut image_data = Vec::new(); @@ -525,8 +522,12 @@ mod frame_capture { info!("Saving image to: {images_dir:?}"); std::fs::create_dir_all(&images_dir).unwrap(); - let number = rng.gen::(); - let image_path = images_dir.join(format!("{number:032X}.png")); + let mut number = 0; + let mut image_path = images_dir.join(format!("{number:03}.png")); + while image_path.exists() { + number += 1; + image_path = images_dir.join(format!("{number:03}.png")); + } if let Err(e) = img.save(image_path) { panic!("Failed to save image: {}", e); }; From 5c8e54ba8abb9e0030396481ed64a23bc542916c Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Mon, 6 May 2024 01:14:12 +0300 Subject: [PATCH 11/14] Simplified image creation, fixed ci build --- examples/app/headless_renderer.rs | 56 ++++++++++++------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 84819331b4fe8..d273d087a9232 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -375,10 +375,10 @@ mod frame_capture { prelude::*, render::{ camera::RenderTarget, - render_resource::{ - Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, - }, + render_asset::RenderAssetUsages, + render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, renderer::RenderDevice, + texture::BevyDefault, }, }; use std::path::PathBuf; @@ -440,40 +440,26 @@ mod frame_capture { }; // This is the texture that will be rendered to. - let mut render_target_image = Image { - texture_descriptor: TextureDescriptor { - label: None, - size, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::COPY_SRC - | TextureUsages::COPY_DST - | TextureUsages::TEXTURE_BINDING - | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }, - ..Default::default() - }; - render_target_image.resize(size); + let mut render_target_image = Image::new_fill( + size, + TextureDimension::D2, + &[0; 4], + TextureFormat::bevy_default(), + RenderAssetUsages::default(), + ); + render_target_image.texture_descriptor.usage |= TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING; let render_target_image_handle = images.add(render_target_image); // This is the texture that will be copied to. - let mut cpu_image = Image { - texture_descriptor: TextureDescriptor { - label: None, - size, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ..Default::default() - }; - cpu_image.resize(size); + let cpu_image = Image::new_fill( + size, + TextureDimension::D2, + &[0; 4], + TextureFormat::bevy_default(), + RenderAssetUsages::default(), + ); let cpu_image_handle = images.add(cpu_image); commands.spawn(ImageCopier::new( @@ -510,7 +496,7 @@ mod frame_capture { if !image_data.is_empty() { for image in images_to_save.iter() { let img_bytes = images.get_mut(image.id()).unwrap(); - img_bytes.data = image_data.clone(); + img_bytes.data.clone_from(&image_data); let img = match img_bytes.clone().try_into_dynamic() { Ok(img) => img.to_rgba8(), From b74e2380cc481876259155d0f8ffc17098453d31 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Mon, 6 May 2024 01:36:52 +0300 Subject: [PATCH 12/14] Interaction with file system has been commented --- examples/app/headless_renderer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index d273d087a9232..518c5edb29ed4 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -495,25 +495,33 @@ mod frame_capture { } if !image_data.is_empty() { for image in images_to_save.iter() { + // Fill correct data from channel to image let img_bytes = images.get_mut(image.id()).unwrap(); img_bytes.data.clone_from(&image_data); + // Create RGBA Image Buffer let img = match img_bytes.clone().try_into_dynamic() { Ok(img) => img.to_rgba8(), Err(e) => panic!("Failed to create image buffer {e:?}"), }; + // Prepare directory for images, test_images in bevy folder is used here for example + // You should choose the path depending on your needs let images_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); info!("Saving image to: {images_dir:?}"); std::fs::create_dir_all(&images_dir).unwrap(); + // Search for the first unoccupied number let mut number = 0; let mut image_path = images_dir.join(format!("{number:03}.png")); while image_path.exists() { number += 1; image_path = images_dir.join(format!("{number:03}.png")); } + + // Finally saving image to file, this heavy blocking operation is kept here + // for example simplicity, but in real app you should move it to a separate task if let Err(e) = img.save(image_path) { panic!("Failed to save image: {}", e); }; From c29cd0b41d35deb18687f886eb30dec2d0a0c2ab Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Mon, 6 May 2024 08:28:00 +0300 Subject: [PATCH 13/14] Rewrite files instead of searching unocupied file numbers --- examples/app/headless_renderer.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 518c5edb29ed4..0cf95589e106d 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -381,7 +381,10 @@ mod frame_capture { texture::BevyDefault, }, }; - use std::path::PathBuf; + use std::{ + ops::{Deref, DerefMut}, + path::PathBuf, + }; #[derive(Component, Default)] pub struct CaptureCamera; @@ -482,6 +485,7 @@ mod frame_capture { mut images: ResMut>, mut scene_controller: ResMut, mut app_exit_writer: EventWriter, + mut file_number: Local, ) { if let SceneState::Render(n) = scene_controller.state { if n < 1 { @@ -512,13 +516,10 @@ mod frame_capture { info!("Saving image to: {images_dir:?}"); std::fs::create_dir_all(&images_dir).unwrap(); - // Search for the first unoccupied number - let mut number = 0; - let mut image_path = images_dir.join(format!("{number:03}.png")); - while image_path.exists() { - number += 1; - image_path = images_dir.join(format!("{number:03}.png")); - } + // Choose filename starting from 000.png + let image_path = + images_dir.join(format!("{:03}.png", file_number.deref())); + *file_number.deref_mut() += 1; // Finally saving image to file, this heavy blocking operation is kept here // for example simplicity, but in real app you should move it to a separate task From 5436d6edcef7f87a5db0cc486657b1579812d90f Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Wed, 8 May 2024 15:23:22 +0300 Subject: [PATCH 14/14] Removed nested modules, added comments --- examples/app/headless_renderer.rs | 749 +++++++++++++++--------------- 1 file changed, 368 insertions(+), 381 deletions(-) diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 0cf95589e106d..e4b634f31130b 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -3,16 +3,39 @@ //! It follows this steps: //! 1. Render from camera to gpu-image render target //! 2. Copy form gpu image to buffer using `ImageCopyDriver` node in `RenderGraph` -//! 3. Copy from buffer to channel using `image_copy::receive_image_from_buffer` after `RenderSet::Render` +//! 3. Copy from buffer to channel using `receive_image_from_buffer` after `RenderSet::Render` //! 4. Save from channel to random named file using `scene::update` at `PostUpdate` in `MainWorld` //! 5. Exit if `single_image` setting is set use bevy::{ - app::ScheduleRunnerPlugin, core_pipeline::tonemapping::Tonemapping, prelude::*, - render::renderer::RenderDevice, + app::{AppExit, ScheduleRunnerPlugin}, + core_pipeline::tonemapping::Tonemapping, + prelude::*, + render::{ + camera::RenderTarget, + render_asset::RenderAssetUsages, + render_asset::RenderAssets, + render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel}, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, + ImageCopyBuffer, ImageDataLayout, Maintain, MapMode, TextureDimension, TextureFormat, + TextureUsages, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::BevyDefault, + Extract, Render, RenderApp, RenderSet, + }, }; - use crossbeam_channel::{Receiver, Sender}; +use std::{ + ops::{Deref, DerefMut}, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; // To communicate between the main world and the render world we need a channel. // Since the main world and render world run in parallel, there will always be a frame of latency @@ -50,7 +73,7 @@ fn main() { // setup frame capture App::new() - .insert_resource(frame_capture::scene::SceneController::new( + .insert_resource(SceneController::new( config.width, config.height, config.single_image, @@ -66,27 +89,59 @@ fn main() { close_when_requested: false, }), ) - .add_plugins(frame_capture::image_copy::ImageCopyPlugin) + .add_plugins(ImageCopyPlugin) // headless frame capture - .add_plugins(frame_capture::scene::CaptureFramePlugin) + .add_plugins(CaptureFramePlugin) .add_plugins(ScheduleRunnerPlugin::run_loop( // Run 60 times per second. - std::time::Duration::from_secs_f64(1.0 / 60.0), + Duration::from_secs_f64(1.0 / 60.0), )) - .init_resource::() + .init_resource::() .add_systems(Startup, setup) .run(); } +/// Capture image settings and state +#[derive(Debug, Default, Resource)] +struct SceneController { + state: SceneState, + name: String, + width: u32, + height: u32, + single_image: bool, +} + +impl SceneController { + pub fn new(width: u32, height: u32, single_image: bool) -> SceneController { + SceneController { + state: SceneState::BuildScene, + name: String::from(""), + width, + height, + single_image, + } + } +} + +/// Capture image state +#[derive(Debug, Default)] +enum SceneState { + #[default] + // State before any rendering + BuildScene, + // Rendering state, stores the number of frames remaining before saving the image + Render(u32), +} + fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, mut images: ResMut>, - mut scene_controller: ResMut, + mut scene_controller: ResMut, render_device: Res, ) { - let render_target = frame_capture::scene::setup_render_target( + let render_target = setup_render_target( &mut commands, &mut images, &render_device, @@ -141,402 +196,334 @@ fn setup( }); } -mod frame_capture { - pub mod image_copy { - use crate::{MainWorldReceiver, RenderWorldSender}; - use bevy::prelude::*; - use bevy::render::{ - render_asset::RenderAssets, - render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel}, - render_resource::{ - Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, - ImageCopyBuffer, ImageDataLayout, Maintain, MapMode, - }, - renderer::{RenderContext, RenderDevice, RenderQueue}, - Extract, Render, RenderApp, RenderSet, - }; - use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }; - - pub fn receive_image_from_buffer( - image_copiers: Res, - render_device: Res, - sender: Res, - ) { - for image_copier in image_copiers.0.iter() { - if !image_copier.enabled() { - continue; - } - - // Finally time to get our data back from the gpu. - // First we get a buffer slice which represents a chunk of the buffer (which we - // can't access yet). - // We want the whole thing so use unbounded range. - let buffer_slice = image_copier.buffer.slice(..); - - // Now things get complicated. WebGPU, for safety reasons, only allows either the GPU - // or CPU to access a buffer's contents at a time. We need to "map" the buffer which means - // flipping ownership of the buffer over to the CPU and making access legal. We do this - // with `BufferSlice::map_async`. - // - // The problem is that map_async is not an async function so we can't await it. What - // we need to do instead is pass in a closure that will be executed when the slice is - // either mapped or the mapping has failed. - // - // The problem with this is that we don't have a reliable way to wait in the main - // code for the buffer to be mapped and even worse, calling get_mapped_range or - // get_mapped_range_mut prematurely will cause a panic, not return an error. - // - // Using channels solves this as awaiting the receiving of a message from - // the passed closure will force the outside code to wait. It also doesn't hurt - // if the closure finishes before the outside code catches up as the message is - // buffered and receiving will just pick that up. - // - // It may also be worth noting that although on native, the usage of asynchronous - // channels is wholly unnecessary, for the sake of portability to WASM - // we'll use async channels that work on both native and WASM. - - let (s, r) = crossbeam_channel::bounded(1); - - // Maps the buffer so it can be read on the cpu - buffer_slice.map_async(MapMode::Read, move |r| match r { - // This will execute once the gpu is ready, so after the call to poll() - Ok(r) => s.send(r).expect("Failed to send map update"), - Err(err) => panic!("Failed to map buffer {err}"), - }); - - // In order for the mapping to be completed, one of three things must happen. - // One of those can be calling `Device::poll`. This isn't necessary on the web as devices - // are polled automatically but natively, we need to make sure this happens manually. - // `Maintain::Wait` will cause the thread to wait on native but not on WebGpu. - - // This blocks until the gpu is done executing everything - render_device.poll(Maintain::wait()).panic_on_timeout(); - - // This blocks until the buffer is mapped - r.recv().expect("Failed to receive the map_async message"); - - // This could fail on app exit, if Main world clears resources (including receiver) while Render world still renders - let _ = sender.send(buffer_slice.get_mapped_range().to_vec()); - - // We need to make sure all `BufferView`'s are dropped before we do what we're about - // to do. - // Unmap so that we can copy to the staging buffer in the next iteration. - image_copier.buffer.unmap(); - } - } +/// Plugin for Render world part of work +pub struct ImageCopyPlugin; +impl Plugin for ImageCopyPlugin { + fn build(&self, app: &mut App) { + let (s, r) = crossbeam_channel::unbounded(); + + let render_app = app + .insert_resource(MainWorldReceiver(r)) + .sub_app_mut(RenderApp); + + let mut graph = render_app.world_mut().resource_mut::(); + graph.add_node(ImageCopy, ImageCopyDriver); + graph.add_node_edge(bevy::render::graph::CameraDriverLabel, ImageCopy); + + render_app + .insert_resource(RenderWorldSender(s)) + // Make ImageCopiers accessible in RenderWorld system and plugin + .add_systems(ExtractSchedule, image_copy_extract) + // Receives image data from buffer to channel + // so we need to run it after the render graph is done + .add_systems(Render, receive_image_from_buffer.after(RenderSet::Render)); + } +} - #[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)] - pub struct ImageCopy; - - // Plugin for Render world part of work - pub struct ImageCopyPlugin; - impl Plugin for ImageCopyPlugin { - fn build(&self, app: &mut App) { - let (s, r) = crossbeam_channel::unbounded(); - - let render_app = app - .insert_resource(MainWorldReceiver(r)) - .sub_app_mut(RenderApp); - - let mut graph = render_app.world_mut().resource_mut::(); - graph.add_node(ImageCopy, ImageCopyDriver); - graph.add_node_edge(bevy::render::graph::CameraDriverLabel, ImageCopy); - - render_app - .insert_resource(RenderWorldSender(s)) - // Make ImageCopiers accessible in RenderWorld system and plugin - .add_systems(ExtractSchedule, image_copy_extract) - // Receives image data from buffer to channel - // so we need to run it after the render graph is done - .add_systems(Render, receive_image_from_buffer.after(RenderSet::Render)); - } - } +/// Setups render target and cpu image for saving, changes scene state into render mode +fn setup_render_target( + commands: &mut Commands, + images: &mut ResMut>, + render_device: &Res, + scene_controller: &mut ResMut, + pre_roll_frames: u32, + scene_name: String, +) -> RenderTarget { + let size = Extent3d { + width: scene_controller.width, + height: scene_controller.height, + ..Default::default() + }; - #[derive(Clone, Default, Resource, Deref, DerefMut)] - pub struct ImageCopiers(pub Vec); + // This is the texture that will be rendered to. + let mut render_target_image = Image::new_fill( + size, + TextureDimension::D2, + &[0; 4], + TextureFormat::bevy_default(), + RenderAssetUsages::default(), + ); + render_target_image.texture_descriptor.usage |= + TextureUsages::COPY_SRC | TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING; + let render_target_image_handle = images.add(render_target_image); + + // This is the texture that will be copied to. + let cpu_image = Image::new_fill( + size, + TextureDimension::D2, + &[0; 4], + TextureFormat::bevy_default(), + RenderAssetUsages::default(), + ); + let cpu_image_handle = images.add(cpu_image); - #[derive(Clone, Component)] - pub struct ImageCopier { - buffer: Buffer, - enabled: Arc, - src_image: Handle, - } + commands.spawn(ImageCopier::new( + render_target_image_handle.clone(), + size, + render_device, + )); - impl ImageCopier { - pub fn new( - src_image: Handle, - size: Extent3d, - render_device: &RenderDevice, - ) -> ImageCopier { - let padded_bytes_per_row = - RenderDevice::align_copy_bytes_per_row((size.width) as usize) * 4; - - let cpu_buffer = render_device.create_buffer(&BufferDescriptor { - label: None, - size: padded_bytes_per_row as u64 * size.height as u64, - usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - ImageCopier { - buffer: cpu_buffer, - src_image, - enabled: Arc::new(AtomicBool::new(true)), - } - } + commands.spawn(ImageToSave(cpu_image_handle)); - pub fn enabled(&self) -> bool { - self.enabled.load(Ordering::Relaxed) - } - } + scene_controller.state = SceneState::Render(pre_roll_frames); + scene_controller.name = scene_name; + RenderTarget::Image(render_target_image_handle) +} - pub fn image_copy_extract( - mut commands: Commands, - image_copiers: Extract>, - ) { - commands.insert_resource(ImageCopiers( - image_copiers.iter().cloned().collect::>(), - )); - } +/// Setups image saver +pub struct CaptureFramePlugin; +impl Plugin for CaptureFramePlugin { + fn build(&self, app: &mut App) { + info!("Adding CaptureFramePlugin"); + app.add_systems(PostUpdate, update); + } +} - #[derive(Default)] - pub struct ImageCopyDriver; - - // Copies image content from render target to buffer - impl render_graph::Node for ImageCopyDriver { - fn run( - &self, - _graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, - ) -> Result<(), NodeRunError> { - let image_copiers = world.get_resource::().unwrap(); - let gpu_images = world - .get_resource::>() - .unwrap(); - - for image_copier in image_copiers.iter() { - if !image_copier.enabled() { - continue; - } - - let src_image = gpu_images.get(&image_copier.src_image).unwrap(); - - let mut encoder = render_context - .render_device() - .create_command_encoder(&CommandEncoderDescriptor::default()); - - let block_dimensions = src_image.texture_format.block_dimensions(); - let block_size = src_image.texture_format.block_copy_size(None).unwrap(); - - let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( - (src_image.size.x as usize / block_dimensions.0 as usize) - * block_size as usize, - ); - - let texture_extent = Extent3d { - width: src_image.size.x, - height: src_image.size.y, - depth_or_array_layers: 1, - }; +/// `ImageCopier` aggregator in `RenderWorld` +#[derive(Clone, Default, Resource, Deref, DerefMut)] +struct ImageCopiers(pub Vec); - encoder.copy_texture_to_buffer( - src_image.texture.as_image_copy(), - ImageCopyBuffer { - buffer: &image_copier.buffer, - layout: ImageDataLayout { - offset: 0, - bytes_per_row: Some( - std::num::NonZeroU32::new(padded_bytes_per_row as u32) - .unwrap() - .into(), - ), - rows_per_image: None, - }, - }, - texture_extent, - ); - - let render_queue = world.get_resource::().unwrap(); - render_queue.submit(std::iter::once(encoder.finish())); - } +/// Used by `ImageCopyDriver` for copying from render target to buffer +#[derive(Clone, Component)] +struct ImageCopier { + buffer: Buffer, + enabled: Arc, + src_image: Handle, +} - Ok(()) - } +impl ImageCopier { + pub fn new( + src_image: Handle, + size: Extent3d, + render_device: &RenderDevice, + ) -> ImageCopier { + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row((size.width) as usize) * 4; + + let cpu_buffer = render_device.create_buffer(&BufferDescriptor { + label: None, + size: padded_bytes_per_row as u64 * size.height as u64, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + ImageCopier { + buffer: cpu_buffer, + src_image, + enabled: Arc::new(AtomicBool::new(true)), } } - pub mod scene { - use super::{super::MainWorldReceiver, image_copy::ImageCopier}; - use bevy::{ - app::AppExit, - prelude::*, - render::{ - camera::RenderTarget, - render_asset::RenderAssetUsages, - render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, - renderer::RenderDevice, - texture::BevyDefault, - }, - }; - use std::{ - ops::{Deref, DerefMut}, - path::PathBuf, - }; - - #[derive(Component, Default)] - pub struct CaptureCamera; - - #[derive(Component, Deref, DerefMut)] - struct ImageToSave(Handle); - - pub struct CaptureFramePlugin; - impl Plugin for CaptureFramePlugin { - fn build(&self, app: &mut App) { - info!("Adding CaptureFramePlugin"); - app.add_systems(PostUpdate, update); - } - } - #[derive(Debug, Default, Resource)] - pub struct SceneController { - state: SceneState, - name: String, - width: u32, - height: u32, - single_image: bool, - } + pub fn enabled(&self) -> bool { + self.enabled.load(Ordering::Relaxed) + } +} - impl SceneController { - pub fn new(width: u32, height: u32, single_image: bool) -> SceneController { - SceneController { - state: SceneState::BuildScene, - name: String::from(""), - width, - height, - single_image, - } +/// Extracting `ImageCopier`s into render world, because `ImageCopyDriver` accesses them +fn image_copy_extract(mut commands: Commands, image_copiers: Extract>) { + commands.insert_resource(ImageCopiers( + image_copiers.iter().cloned().collect::>(), + )); +} + +/// `RenderGraph` label for `ImageCopyDriver` +#[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)] +struct ImageCopy; + +/// `RenderGraph` node +#[derive(Default)] +struct ImageCopyDriver; + +// Copies image content from render target to buffer +impl render_graph::Node for ImageCopyDriver { + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let image_copiers = world.get_resource::().unwrap(); + let gpu_images = world + .get_resource::>() + .unwrap(); + + for image_copier in image_copiers.iter() { + if !image_copier.enabled() { + continue; } - } - #[derive(Debug, Default)] - pub enum SceneState { - #[default] - BuildScene, - Render(u32), - } + let src_image = gpu_images.get(&image_copier.src_image).unwrap(); - pub fn setup_render_target( - commands: &mut Commands, - images: &mut ResMut>, - render_device: &Res, - scene_controller: &mut ResMut, - pre_roll_frames: u32, - scene_name: String, - ) -> RenderTarget { - let size = Extent3d { - width: scene_controller.width, - height: scene_controller.height, - ..Default::default() - }; + let mut encoder = render_context + .render_device() + .create_command_encoder(&CommandEncoderDescriptor::default()); + + let block_dimensions = src_image.texture_format.block_dimensions(); + let block_size = src_image.texture_format.block_copy_size(None).unwrap(); - // This is the texture that will be rendered to. - let mut render_target_image = Image::new_fill( - size, - TextureDimension::D2, - &[0; 4], - TextureFormat::bevy_default(), - RenderAssetUsages::default(), + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( + (src_image.size.x as usize / block_dimensions.0 as usize) * block_size as usize, ); - render_target_image.texture_descriptor.usage |= TextureUsages::COPY_SRC - | TextureUsages::RENDER_ATTACHMENT - | TextureUsages::TEXTURE_BINDING; - let render_target_image_handle = images.add(render_target_image); - - // This is the texture that will be copied to. - let cpu_image = Image::new_fill( - size, - TextureDimension::D2, - &[0; 4], - TextureFormat::bevy_default(), - RenderAssetUsages::default(), + + let texture_extent = Extent3d { + width: src_image.size.x, + height: src_image.size.y, + depth_or_array_layers: 1, + }; + + encoder.copy_texture_to_buffer( + src_image.texture.as_image_copy(), + ImageCopyBuffer { + buffer: &image_copier.buffer, + layout: ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new(padded_bytes_per_row as u32) + .unwrap() + .into(), + ), + rows_per_image: None, + }, + }, + texture_extent, ); - let cpu_image_handle = images.add(cpu_image); - commands.spawn(ImageCopier::new( - render_target_image_handle.clone(), - size, - render_device, - )); + let render_queue = world.get_resource::().unwrap(); + render_queue.submit(std::iter::once(encoder.finish())); + } - commands.spawn(ImageToSave(cpu_image_handle)); + Ok(()) + } +} - scene_controller.state = SceneState::Render(pre_roll_frames); - scene_controller.name = scene_name; - RenderTarget::Image(render_target_image_handle) +/// runs in render world after Render stage to send image from buffer via channel (receiver is in main world) +fn receive_image_from_buffer( + image_copiers: Res, + render_device: Res, + sender: Res, +) { + for image_copier in image_copiers.0.iter() { + if !image_copier.enabled() { + continue; } - // Takes from channel image content sent from render world and saves it to disk - fn update( - images_to_save: Query<&ImageToSave>, - receiver: Res, - mut images: ResMut>, - mut scene_controller: ResMut, - mut app_exit_writer: EventWriter, - mut file_number: Local, - ) { - if let SceneState::Render(n) = scene_controller.state { - if n < 1 { - // We don't want to block the main world on this, - // so we use try_recv which attempts to receive without blocking - let mut image_data = Vec::new(); - while let Ok(data) = receiver.try_recv() { - // image generation could be faster than saving to fs, - // that's why use only last of them - image_data = data; - } - if !image_data.is_empty() { - for image in images_to_save.iter() { - // Fill correct data from channel to image - let img_bytes = images.get_mut(image.id()).unwrap(); - img_bytes.data.clone_from(&image_data); - - // Create RGBA Image Buffer - let img = match img_bytes.clone().try_into_dynamic() { - Ok(img) => img.to_rgba8(), - Err(e) => panic!("Failed to create image buffer {e:?}"), - }; - - // Prepare directory for images, test_images in bevy folder is used here for example - // You should choose the path depending on your needs - let images_dir = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); - info!("Saving image to: {images_dir:?}"); - std::fs::create_dir_all(&images_dir).unwrap(); - - // Choose filename starting from 000.png - let image_path = - images_dir.join(format!("{:03}.png", file_number.deref())); - *file_number.deref_mut() += 1; - - // Finally saving image to file, this heavy blocking operation is kept here - // for example simplicity, but in real app you should move it to a separate task - if let Err(e) = img.save(image_path) { - panic!("Failed to save image: {}", e); - }; - } - if scene_controller.single_image { - app_exit_writer.send(AppExit::Success); - } - } - } else { - // clears channel for skipped frames - while receiver.try_recv().is_ok() {} - scene_controller.state = SceneState::Render(n - 1); + // Finally time to get our data back from the gpu. + // First we get a buffer slice which represents a chunk of the buffer (which we + // can't access yet). + // We want the whole thing so use unbounded range. + let buffer_slice = image_copier.buffer.slice(..); + + // Now things get complicated. WebGPU, for safety reasons, only allows either the GPU + // or CPU to access a buffer's contents at a time. We need to "map" the buffer which means + // flipping ownership of the buffer over to the CPU and making access legal. We do this + // with `BufferSlice::map_async`. + // + // The problem is that map_async is not an async function so we can't await it. What + // we need to do instead is pass in a closure that will be executed when the slice is + // either mapped or the mapping has failed. + // + // The problem with this is that we don't have a reliable way to wait in the main + // code for the buffer to be mapped and even worse, calling get_mapped_range or + // get_mapped_range_mut prematurely will cause a panic, not return an error. + // + // Using channels solves this as awaiting the receiving of a message from + // the passed closure will force the outside code to wait. It also doesn't hurt + // if the closure finishes before the outside code catches up as the message is + // buffered and receiving will just pick that up. + // + // It may also be worth noting that although on native, the usage of asynchronous + // channels is wholly unnecessary, for the sake of portability to WASM + // we'll use async channels that work on both native and WASM. + + let (s, r) = crossbeam_channel::bounded(1); + + // Maps the buffer so it can be read on the cpu + buffer_slice.map_async(MapMode::Read, move |r| match r { + // This will execute once the gpu is ready, so after the call to poll() + Ok(r) => s.send(r).expect("Failed to send map update"), + Err(err) => panic!("Failed to map buffer {err}"), + }); + + // In order for the mapping to be completed, one of three things must happen. + // One of those can be calling `Device::poll`. This isn't necessary on the web as devices + // are polled automatically but natively, we need to make sure this happens manually. + // `Maintain::Wait` will cause the thread to wait on native but not on WebGpu. + + // This blocks until the gpu is done executing everything + render_device.poll(Maintain::wait()).panic_on_timeout(); + + // This blocks until the buffer is mapped + r.recv().expect("Failed to receive the map_async message"); + + // This could fail on app exit, if Main world clears resources (including receiver) while Render world still renders + let _ = sender.send(buffer_slice.get_mapped_range().to_vec()); + + // We need to make sure all `BufferView`'s are dropped before we do what we're about + // to do. + // Unmap so that we can copy to the staging buffer in the next iteration. + image_copier.buffer.unmap(); + } +} + +/// CPU-side image for saving +#[derive(Component, Deref, DerefMut)] +struct ImageToSave(Handle); + +// Takes from channel image content sent from render world and saves it to disk +fn update( + images_to_save: Query<&ImageToSave>, + receiver: Res, + mut images: ResMut>, + mut scene_controller: ResMut, + mut app_exit_writer: EventWriter, + mut file_number: Local, +) { + if let SceneState::Render(n) = scene_controller.state { + if n < 1 { + // We don't want to block the main world on this, + // so we use try_recv which attempts to receive without blocking + let mut image_data = Vec::new(); + while let Ok(data) = receiver.try_recv() { + // image generation could be faster than saving to fs, + // that's why use only last of them + image_data = data; + } + if !image_data.is_empty() { + for image in images_to_save.iter() { + // Fill correct data from channel to image + let img_bytes = images.get_mut(image.id()).unwrap(); + img_bytes.data.clone_from(&image_data); + + // Create RGBA Image Buffer + let img = match img_bytes.clone().try_into_dynamic() { + Ok(img) => img.to_rgba8(), + Err(e) => panic!("Failed to create image buffer {e:?}"), + }; + + // Prepare directory for images, test_images in bevy folder is used here for example + // You should choose the path depending on your needs + let images_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); + info!("Saving image to: {images_dir:?}"); + std::fs::create_dir_all(&images_dir).unwrap(); + + // Choose filename starting from 000.png + let image_path = images_dir.join(format!("{:03}.png", file_number.deref())); + *file_number.deref_mut() += 1; + + // Finally saving image to file, this heavy blocking operation is kept here + // for example simplicity, but in real app you should move it to a separate task + if let Err(e) = img.save(image_path) { + panic!("Failed to save image: {}", e); + }; + } + if scene_controller.single_image { + app_exit_writer.send(AppExit::Success); } } + } else { + // clears channel for skipped frames + while receiver.try_recv().is_ok() {} + scene_controller.state = SceneState::Render(n - 1); } } }