headless_renderer/
headless_renderer.rs

1//! This example illustrates how to make headless renderer
2//! derived from: <https://sotrh.github.io/learn-wgpu/showcase/windowless/#a-triangle-without-a-window>
3//! It follows this steps:
4//! 1. Render from camera to gpu-image render target
5//! 2. Copy from gpu image to buffer using `ImageCopyDriver` node in `RenderGraph`
6//! 3. Copy from buffer to channel using `receive_image_from_buffer` after `RenderSet::Render`
7//! 4. Save from channel to random named file using `scene::update` at `PostUpdate` in `MainWorld`
8//! 5. Exit if `single_image` setting is set
9
10use 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// To communicate between the main world and the render world we need a channel.
41// Since the main world and render world run in parallel, there will always be a frame of latency
42// between the data sent from the render world and the data received in the main world
43//
44// frame n => render world sends data through the channel at the end of the frame
45// frame n + 1 => main world receives the data
46//
47// Receiver and Sender are kept in resources because there is single camera and single target
48// That's why there is single images role, if you want to differentiate images
49// from different cameras, you should keep Receiver in ImageCopier and Sender in ImageToSave
50// or send some id with data
51
52/// This will receive asynchronously any data sent from the render world
53#[derive(Resource, Deref)]
54struct MainWorldReceiver(Receiver<Vec<u8>>);
55
56/// This will send asynchronously any data to the main world
57#[derive(Resource, Deref)]
58struct RenderWorldSender(Sender<Vec<u8>>);
59
60// Parameters of resulting image
61struct 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    // setup frame capture
75    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                // Not strictly necessary, as the inclusion of ScheduleRunnerPlugin below
86                // replaces the bevy_winit app runner and so a window is never created.
87                .set(WindowPlugin {
88                    primary_window: None,
89                    ..default()
90                })
91                // WinitPlugin will panic in environments without a display server.
92                .disable::<WinitPlugin>(),
93        )
94        .add_plugins(ImageCopyPlugin)
95        // headless frame capture
96        .add_plugins(CaptureFramePlugin)
97        // ScheduleRunnerPlugin provides an alternative to the default bevy_winit app runner, which
98        // manages the loop without creating a window.
99        .add_plugins(ScheduleRunnerPlugin::run_loop(
100            // Run 60 times per second.
101            Duration::from_secs_f64(1.0 / 60.0),
102        ))
103        .init_resource::<SceneController>()
104        .add_systems(Startup, setup)
105        .run();
106}
107
108/// Capture image settings and state
109#[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/// Capture image state
131#[derive(Debug, Default)]
132enum SceneState {
133    #[default]
134    // State before any rendering
135    BuildScene,
136    // Rendering state, stores the number of frames remaining before saving the image
137    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        // pre_roll_frames should be big enough for full scene render,
154        // but the bigger it is, the longer example will run.
155        // To visualize stages of scene rendering change this param to 0
156        // and change AppConfig::single_image to false in main
157        // Stages are:
158        // 1. Transparent image
159        // 2. Few black box images
160        // 3. Fully rendered scene images
161        // Exact number depends on device speed, device load and scene size
162        40,
163        "main_scene".into(),
164    );
165
166    // Scene example for non black box picture
167    // circular base
168    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    // cube
174    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    // light
180    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            // render to image
192            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
200/// Plugin for Render world part of work
201pub 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            // Make ImageCopiers accessible in RenderWorld system and plugin
217            .add_systems(ExtractSchedule, image_copy_extract)
218            // Receives image data from buffer to channel
219            // so we need to run it after the render graph is done
220            .add_systems(Render, receive_image_from_buffer.after(RenderSet::Render));
221    }
222}
223
224/// Setups render target and cpu image for saving, changes scene state into render mode
225fn 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    // This is the texture that will be rendered to.
240    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    // This is the texture that will be copied to.
252    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
274/// Setups image saver
275pub 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/// `ImageCopier` aggregator in `RenderWorld`
284#[derive(Clone, Default, Resource, Deref, DerefMut)]
285struct ImageCopiers(pub Vec<ImageCopier>);
286
287/// Used by `ImageCopyDriver` for copying from render target to buffer
288#[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
323/// Extracting `ImageCopier`s into render world, because `ImageCopyDriver` accesses them
324fn 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/// `RenderGraph` label for `ImageCopyDriver`
331#[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)]
332struct ImageCopy;
333
334/// `RenderGraph` node
335#[derive(Default)]
336struct ImageCopyDriver;
337
338// Copies image content from render target to buffer
339impl 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            // Calculating correct size of image row because
366            // copy_texture_to_buffer can copy image only by rows aligned wgpu::COPY_BYTES_PER_ROW_ALIGNMENT
367            // That's why image in buffer can be little bit wider
368            // This should be taken into account at copy from buffer stage
369            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
398/// runs in render world after Render stage to send image from buffer via channel (receiver is in main world)
399fn 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        // Finally time to get our data back from the gpu.
410        // First we get a buffer slice which represents a chunk of the buffer (which we
411        // can't access yet).
412        // We want the whole thing so use unbounded range.
413        let buffer_slice = image_copier.buffer.slice(..);
414
415        // Now things get complicated. WebGPU, for safety reasons, only allows either the GPU
416        // or CPU to access a buffer's contents at a time. We need to "map" the buffer which means
417        // flipping ownership of the buffer over to the CPU and making access legal. We do this
418        // with `BufferSlice::map_async`.
419        //
420        // The problem is that map_async is not an async function so we can't await it. What
421        // we need to do instead is pass in a closure that will be executed when the slice is
422        // either mapped or the mapping has failed.
423        //
424        // The problem with this is that we don't have a reliable way to wait in the main
425        // code for the buffer to be mapped and even worse, calling get_mapped_range or
426        // get_mapped_range_mut prematurely will cause a panic, not return an error.
427        //
428        // Using channels solves this as awaiting the receiving of a message from
429        // the passed closure will force the outside code to wait. It also doesn't hurt
430        // if the closure finishes before the outside code catches up as the message is
431        // buffered and receiving will just pick that up.
432        //
433        // It may also be worth noting that although on native, the usage of asynchronous
434        // channels is wholly unnecessary, for the sake of portability to Wasm
435        // we'll use async channels that work on both native and Wasm.
436
437        let (s, r) = crossbeam_channel::bounded(1);
438
439        // Maps the buffer so it can be read on the cpu
440        buffer_slice.map_async(MapMode::Read, move |r| match r {
441            // This will execute once the gpu is ready, so after the call to poll()
442            Ok(r) => s.send(r).expect("Failed to send map update"),
443            Err(err) => panic!("Failed to map buffer {err}"),
444        });
445
446        // In order for the mapping to be completed, one of three things must happen.
447        // One of those can be calling `Device::poll`. This isn't necessary on the web as devices
448        // are polled automatically but natively, we need to make sure this happens manually.
449        // `Maintain::Wait` will cause the thread to wait on native but not on WebGpu.
450
451        // This blocks until the gpu is done executing everything
452        render_device.poll(Maintain::wait()).panic_on_timeout();
453
454        // This blocks until the buffer is mapped
455        r.recv().expect("Failed to receive the map_async message");
456
457        // This could fail on app exit, if Main world clears resources (including receiver) while Render world still renders
458        let _ = sender.send(buffer_slice.get_mapped_range().to_vec());
459
460        // We need to make sure all `BufferView`'s are dropped before we do what we're about
461        // to do.
462        // Unmap so that we can copy to the staging buffer in the next iteration.
463        image_copier.buffer.unmap();
464    }
465}
466
467/// CPU-side image for saving
468#[derive(Component, Deref, DerefMut)]
469struct ImageToSave(Handle<Image>);
470
471// Takes from channel image content sent from render world and saves it to disk
472fn 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            // We don't want to block the main world on this,
483            // so we use try_recv which attempts to receive without blocking
484            let mut image_data = Vec::new();
485            while let Ok(data) = receiver.try_recv() {
486                // image generation could be faster than saving to fs,
487                // that's why use only last of them
488                image_data = data;
489            }
490            if !image_data.is_empty() {
491                for image in images_to_save.iter() {
492                    // Fill correct data from channel to image
493                    let img_bytes = images.get_mut(image.id()).unwrap();
494
495                    // We need to ensure that this works regardless of the image dimensions
496                    // If the image became wider when copying from the texture to the buffer,
497                    // then the data is reduced to its original size when copying from the buffer to the image.
498                    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                        // shrink data to original image size
505                        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                    // Create RGBA Image Buffer
516                    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                    // Prepare directory for images, test_images in bevy folder is used here for example
522                    // You should choose the path depending on your needs
523                    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                    // Choose filename starting from 000.png
528                    let image_path = images_dir.join(format!("{:03}.png", file_number.deref()));
529                    *file_number.deref_mut() += 1;
530
531                    // Finally saving image to file, this heavy blocking operation is kept here
532                    // for example simplicity, but in real app you should move it to a separate task
533                    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            // clears channel for skipped frames
543            while receiver.try_recv().is_ok() {}
544            scene_controller.state = SceneState::Render(n - 1);
545        }
546    }
547}