1use bevy::{
11 app::{AppExit, ScheduleRunnerPlugin},
12 core_pipeline::tonemapping::Tonemapping,
13 image::TextureFormatPixelInfo,
14 prelude::*,
15 render::{
16 camera::RenderTarget,
17 render_asset::{RenderAssetUsages, RenderAssets},
18 render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel},
19 render_resource::{
20 Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, Maintain,
21 MapMode, TexelCopyBufferInfo, TexelCopyBufferLayout, TextureDimension, TextureFormat,
22 TextureUsages,
23 },
24 renderer::{RenderContext, RenderDevice, RenderQueue},
25 Extract, Render, RenderApp, RenderSet,
26 },
27 winit::WinitPlugin,
28};
29use crossbeam_channel::{Receiver, Sender};
30use std::{
31 ops::{Deref, DerefMut},
32 path::PathBuf,
33 sync::{
34 atomic::{AtomicBool, Ordering},
35 Arc,
36 },
37 time::Duration,
38};
39
40#[derive(Resource, Deref)]
54struct MainWorldReceiver(Receiver<Vec<u8>>);
55
56#[derive(Resource, Deref)]
58struct RenderWorldSender(Sender<Vec<u8>>);
59
60struct AppConfig {
62 width: u32,
63 height: u32,
64 single_image: bool,
65}
66
67fn main() {
68 let config = AppConfig {
69 width: 1920,
70 height: 1080,
71 single_image: true,
72 };
73
74 App::new()
76 .insert_resource(SceneController::new(
77 config.width,
78 config.height,
79 config.single_image,
80 ))
81 .insert_resource(ClearColor(Color::srgb_u8(0, 0, 0)))
82 .add_plugins(
83 DefaultPlugins
84 .set(ImagePlugin::default_nearest())
85 .set(WindowPlugin {
88 primary_window: None,
89 ..default()
90 })
91 .disable::<WinitPlugin>(),
93 )
94 .add_plugins(ImageCopyPlugin)
95 .add_plugins(CaptureFramePlugin)
97 .add_plugins(ScheduleRunnerPlugin::run_loop(
100 Duration::from_secs_f64(1.0 / 60.0),
102 ))
103 .init_resource::<SceneController>()
104 .add_systems(Startup, setup)
105 .run();
106}
107
108#[derive(Debug, Default, Resource)]
110struct SceneController {
111 state: SceneState,
112 name: String,
113 width: u32,
114 height: u32,
115 single_image: bool,
116}
117
118impl SceneController {
119 pub fn new(width: u32, height: u32, single_image: bool) -> SceneController {
120 SceneController {
121 state: SceneState::BuildScene,
122 name: String::from(""),
123 width,
124 height,
125 single_image,
126 }
127 }
128}
129
130#[derive(Debug, Default)]
132enum SceneState {
133 #[default]
134 BuildScene,
136 Render(u32),
138}
139
140fn setup(
141 mut commands: Commands,
142 mut meshes: ResMut<Assets<Mesh>>,
143 mut materials: ResMut<Assets<StandardMaterial>>,
144 mut images: ResMut<Assets<Image>>,
145 mut scene_controller: ResMut<SceneController>,
146 render_device: Res<RenderDevice>,
147) {
148 let render_target = setup_render_target(
149 &mut commands,
150 &mut images,
151 &render_device,
152 &mut scene_controller,
153 40,
163 "main_scene".into(),
164 );
165
166 commands.spawn((
169 Mesh3d(meshes.add(Circle::new(4.0))),
170 MeshMaterial3d(materials.add(Color::WHITE)),
171 Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
172 ));
173 commands.spawn((
175 Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
176 MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
177 Transform::from_xyz(0.0, 0.5, 0.0),
178 ));
179 commands.spawn((
181 PointLight {
182 shadows_enabled: true,
183 ..default()
184 },
185 Transform::from_xyz(4.0, 8.0, 4.0),
186 ));
187
188 commands.spawn((
189 Camera3d::default(),
190 Camera {
191 target: render_target,
193 ..default()
194 },
195 Tonemapping::None,
196 Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
197 ));
198}
199
200pub struct ImageCopyPlugin;
202impl Plugin for ImageCopyPlugin {
203 fn build(&self, app: &mut App) {
204 let (s, r) = crossbeam_channel::unbounded();
205
206 let render_app = app
207 .insert_resource(MainWorldReceiver(r))
208 .sub_app_mut(RenderApp);
209
210 let mut graph = render_app.world_mut().resource_mut::<RenderGraph>();
211 graph.add_node(ImageCopy, ImageCopyDriver);
212 graph.add_node_edge(bevy::render::graph::CameraDriverLabel, ImageCopy);
213
214 render_app
215 .insert_resource(RenderWorldSender(s))
216 .add_systems(ExtractSchedule, image_copy_extract)
218 .add_systems(Render, receive_image_from_buffer.after(RenderSet::Render));
221 }
222}
223
224fn setup_render_target(
226 commands: &mut Commands,
227 images: &mut ResMut<Assets<Image>>,
228 render_device: &Res<RenderDevice>,
229 scene_controller: &mut ResMut<SceneController>,
230 pre_roll_frames: u32,
231 scene_name: String,
232) -> RenderTarget {
233 let size = Extent3d {
234 width: scene_controller.width,
235 height: scene_controller.height,
236 ..Default::default()
237 };
238
239 let mut render_target_image = Image::new_fill(
241 size,
242 TextureDimension::D2,
243 &[0; 4],
244 TextureFormat::bevy_default(),
245 RenderAssetUsages::default(),
246 );
247 render_target_image.texture_descriptor.usage |=
248 TextureUsages::COPY_SRC | TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING;
249 let render_target_image_handle = images.add(render_target_image);
250
251 let cpu_image = Image::new_fill(
253 size,
254 TextureDimension::D2,
255 &[0; 4],
256 TextureFormat::bevy_default(),
257 RenderAssetUsages::default(),
258 );
259 let cpu_image_handle = images.add(cpu_image);
260
261 commands.spawn(ImageCopier::new(
262 render_target_image_handle.clone(),
263 size,
264 render_device,
265 ));
266
267 commands.spawn(ImageToSave(cpu_image_handle));
268
269 scene_controller.state = SceneState::Render(pre_roll_frames);
270 scene_controller.name = scene_name;
271 RenderTarget::Image(render_target_image_handle.into())
272}
273
274pub struct CaptureFramePlugin;
276impl Plugin for CaptureFramePlugin {
277 fn build(&self, app: &mut App) {
278 info!("Adding CaptureFramePlugin");
279 app.add_systems(PostUpdate, update);
280 }
281}
282
283#[derive(Clone, Default, Resource, Deref, DerefMut)]
285struct ImageCopiers(pub Vec<ImageCopier>);
286
287#[derive(Clone, Component)]
289struct ImageCopier {
290 buffer: Buffer,
291 enabled: Arc<AtomicBool>,
292 src_image: Handle<Image>,
293}
294
295impl ImageCopier {
296 pub fn new(
297 src_image: Handle<Image>,
298 size: Extent3d,
299 render_device: &RenderDevice,
300 ) -> ImageCopier {
301 let padded_bytes_per_row =
302 RenderDevice::align_copy_bytes_per_row((size.width) as usize) * 4;
303
304 let cpu_buffer = render_device.create_buffer(&BufferDescriptor {
305 label: None,
306 size: padded_bytes_per_row as u64 * size.height as u64,
307 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
308 mapped_at_creation: false,
309 });
310
311 ImageCopier {
312 buffer: cpu_buffer,
313 src_image,
314 enabled: Arc::new(AtomicBool::new(true)),
315 }
316 }
317
318 pub fn enabled(&self) -> bool {
319 self.enabled.load(Ordering::Relaxed)
320 }
321}
322
323fn image_copy_extract(mut commands: Commands, image_copiers: Extract<Query<&ImageCopier>>) {
325 commands.insert_resource(ImageCopiers(
326 image_copiers.iter().cloned().collect::<Vec<ImageCopier>>(),
327 ));
328}
329
330#[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)]
332struct ImageCopy;
333
334#[derive(Default)]
336struct ImageCopyDriver;
337
338impl render_graph::Node for ImageCopyDriver {
340 fn run(
341 &self,
342 _graph: &mut RenderGraphContext,
343 render_context: &mut RenderContext,
344 world: &World,
345 ) -> Result<(), NodeRunError> {
346 let image_copiers = world.get_resource::<ImageCopiers>().unwrap();
347 let gpu_images = world
348 .get_resource::<RenderAssets<bevy::render::texture::GpuImage>>()
349 .unwrap();
350
351 for image_copier in image_copiers.iter() {
352 if !image_copier.enabled() {
353 continue;
354 }
355
356 let src_image = gpu_images.get(&image_copier.src_image).unwrap();
357
358 let mut encoder = render_context
359 .render_device()
360 .create_command_encoder(&CommandEncoderDescriptor::default());
361
362 let block_dimensions = src_image.texture_format.block_dimensions();
363 let block_size = src_image.texture_format.block_copy_size(None).unwrap();
364
365 let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row(
370 (src_image.size.width as usize / block_dimensions.0 as usize) * block_size as usize,
371 );
372
373 encoder.copy_texture_to_buffer(
374 src_image.texture.as_image_copy(),
375 TexelCopyBufferInfo {
376 buffer: &image_copier.buffer,
377 layout: TexelCopyBufferLayout {
378 offset: 0,
379 bytes_per_row: Some(
380 std::num::NonZero::<u32>::new(padded_bytes_per_row as u32)
381 .unwrap()
382 .into(),
383 ),
384 rows_per_image: None,
385 },
386 },
387 src_image.size,
388 );
389
390 let render_queue = world.get_resource::<RenderQueue>().unwrap();
391 render_queue.submit(std::iter::once(encoder.finish()));
392 }
393
394 Ok(())
395 }
396}
397
398fn receive_image_from_buffer(
400 image_copiers: Res<ImageCopiers>,
401 render_device: Res<RenderDevice>,
402 sender: Res<RenderWorldSender>,
403) {
404 for image_copier in image_copiers.0.iter() {
405 if !image_copier.enabled() {
406 continue;
407 }
408
409 let buffer_slice = image_copier.buffer.slice(..);
414
415 let (s, r) = crossbeam_channel::bounded(1);
438
439 buffer_slice.map_async(MapMode::Read, move |r| match r {
441 Ok(r) => s.send(r).expect("Failed to send map update"),
443 Err(err) => panic!("Failed to map buffer {err}"),
444 });
445
446 render_device.poll(Maintain::wait()).panic_on_timeout();
453
454 r.recv().expect("Failed to receive the map_async message");
456
457 let _ = sender.send(buffer_slice.get_mapped_range().to_vec());
459
460 image_copier.buffer.unmap();
464 }
465}
466
467#[derive(Component, Deref, DerefMut)]
469struct ImageToSave(Handle<Image>);
470
471fn update(
473 images_to_save: Query<&ImageToSave>,
474 receiver: Res<MainWorldReceiver>,
475 mut images: ResMut<Assets<Image>>,
476 mut scene_controller: ResMut<SceneController>,
477 mut app_exit_writer: EventWriter<AppExit>,
478 mut file_number: Local<u32>,
479) {
480 if let SceneState::Render(n) = scene_controller.state {
481 if n < 1 {
482 let mut image_data = Vec::new();
485 while let Ok(data) = receiver.try_recv() {
486 image_data = data;
489 }
490 if !image_data.is_empty() {
491 for image in images_to_save.iter() {
492 let img_bytes = images.get_mut(image.id()).unwrap();
494
495 let row_bytes = img_bytes.width() as usize
499 * img_bytes.texture_descriptor.format.pixel_size();
500 let aligned_row_bytes = RenderDevice::align_copy_bytes_per_row(row_bytes);
501 if row_bytes == aligned_row_bytes {
502 img_bytes.data.as_mut().unwrap().clone_from(&image_data);
503 } else {
504 img_bytes.data = Some(
506 image_data
507 .chunks(aligned_row_bytes)
508 .take(img_bytes.height() as usize)
509 .flat_map(|row| &row[..row_bytes.min(row.len())])
510 .cloned()
511 .collect(),
512 );
513 }
514
515 let img = match img_bytes.clone().try_into_dynamic() {
517 Ok(img) => img.to_rgba8(),
518 Err(e) => panic!("Failed to create image buffer {e:?}"),
519 };
520
521 let images_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images");
524 info!("Saving image to: {images_dir:?}");
525 std::fs::create_dir_all(&images_dir).unwrap();
526
527 let image_path = images_dir.join(format!("{:03}.png", file_number.deref()));
529 *file_number.deref_mut() += 1;
530
531 if let Err(e) = img.save(image_path) {
534 panic!("Failed to save image: {e}");
535 };
536 }
537 if scene_controller.single_image {
538 app_exit_writer.write(AppExit::Success);
539 }
540 }
541 } else {
542 while receiver.try_recv().is_ok() {}
544 scene_controller.state = SceneState::Render(n - 1);
545 }
546 }
547}