Skip to main content

bevy_render/
pipelined_rendering.rs

1use core::panic::AssertUnwindSafe;
2use std::panic::{catch_unwind, resume_unwind};
3
4use async_channel::{Receiver, Sender};
5
6use bevy_app::{App, AppExit, Plugin, SubApp};
7use bevy_derive::AppLabel;
8use bevy_ecs::{
9    resource::Resource,
10    schedule::MainThreadExecutor,
11    world::{Mut, World},
12};
13use bevy_tasks::ComputeTaskPool;
14
15use crate::RenderApp;
16
17/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread.
18///
19/// The Main schedule of this app can be used to run logic after the render schedule starts, but
20/// before I/O processing. This can be useful for something like frame pacing.
21#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel, Default)]
22pub struct RenderExtractApp;
23
24/// Channels used by the main app to send and receive the render app.
25#[derive(Resource)]
26pub struct RenderAppChannels {
27    app_to_render_sender: Sender<SubApp>,
28    render_to_app_receiver: Receiver<SubApp>,
29    render_app_in_render_thread: bool,
30    /// Pumped during shutdown so the render app's main-thread tasks can finish.
31    main_thread_executor: MainThreadExecutor,
32}
33
34impl RenderAppChannels {
35    /// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`].
36    ///
37    /// `main_thread_executor` must be the [`MainThreadExecutor`] shared with the render world.
38    /// It is pumped during shutdown so the final render update can complete.
39    pub fn new(
40        app_to_render_sender: Sender<SubApp>,
41        render_to_app_receiver: Receiver<SubApp>,
42        main_thread_executor: MainThreadExecutor,
43    ) -> Self {
44        Self {
45            app_to_render_sender,
46            render_to_app_receiver,
47            render_app_in_render_thread: false,
48            main_thread_executor,
49        }
50    }
51
52    /// Send the `render_app` to the rendering thread.
53    pub fn send_blocking(&mut self, render_app: SubApp) {
54        self.app_to_render_sender.send_blocking(render_app).unwrap();
55        self.render_app_in_render_thread = true;
56    }
57
58    /// Receive the `render_app` from the rendering thread.
59    /// Return `None` if the render thread has panicked.
60    pub async fn recv(&mut self) -> Option<SubApp> {
61        let render_app = self.render_to_app_receiver.recv().await.ok()?;
62        self.render_app_in_render_thread = false;
63        Some(render_app)
64    }
65}
66
67impl Drop for RenderAppChannels {
68    fn drop(&mut self) {
69        if self.render_app_in_render_thread {
70            // The render world's non-send data was initialized on the main thread,
71            // so wait for the render app to return and drop it here.
72            //
73            // A blocking receive would deadlock if the final render update has queued
74            // main-thread tasks. Pump their executor while waiting, as renderer_extract does.
75            let result = catch_unwind(AssertUnwindSafe(|| {
76                ComputeTaskPool::get().scope_with_executor(
77                    true,
78                    Some(&self.main_thread_executor.0),
79                    |scope| {
80                        scope.spawn(async { self.render_to_app_receiver.recv().await.ok() });
81                    },
82                );
83            }));
84
85            if let Err(payload) = result {
86                if std::thread::panicking() {
87                    // The scope and the recovered render app's destructors can panic.
88                    // Preserve an existing panic instead of aborting with a double panic.
89                    // Even dropping the new panic's payload could panic again.
90                    core::mem::forget(payload);
91                } else {
92                    resume_unwind(payload);
93                }
94            }
95        }
96    }
97}
98
99/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering.
100///
101/// This moves rendering into a different thread, so that the Nth frame's rendering can
102/// be run at the same time as the N + 1 frame's simulation.
103///
104/// ```text
105/// |--------------------|--------------------|--------------------|--------------------|
106/// | simulation thread  | frame 1 simulation | frame 2 simulation | frame 3 simulation |
107/// |--------------------|--------------------|--------------------|--------------------|
108/// | rendering thread   |                    | frame 1 rendering  | frame 2 rendering  |
109/// |--------------------|--------------------|--------------------|--------------------|
110/// ```
111///
112/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must
113/// be added after that plugin. If it is not added after, the plugin will do nothing.
114///
115/// A single frame of execution looks something like below
116///
117/// ```text
118/// |---------------------------------------------------------------------------|
119/// |      |         | RenderExtractApp schedule | winit events | main schedule |
120/// | sync | extract |----------------------------------------------------------|
121/// |      |         | extract commands | rendering schedule                    |
122/// |---------------------------------------------------------------------------|
123/// ```
124///
125/// - `sync` is the step where the entity-entity mapping between the main and render world is updated.
126///   This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`].
127/// - `extract` is the step where data is copied from the main world to the render world.
128///   This is run on the main app's thread.
129/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the
130///   main schedule can start sooner.
131/// - Then the `rendering schedule` is run. See [`RenderSystems`](crate::RenderSystems) for the standard steps in this process.
132/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By
133///   default, this schedule is empty. But it is useful if you need something to run before I/O processing.
134/// - Next all the `winit events` are processed.
135/// - And finally the `main app schedule` is run.
136/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.
137///
138/// [`SyncWorldPlugin`]: bevy_extract::sync_world::SyncWorldPlugin
139#[derive(Default)]
140pub struct PipelinedRenderingPlugin;
141
142impl Plugin for PipelinedRenderingPlugin {
143    fn build(&self, app: &mut App) {
144        // Don't add RenderExtractApp if RenderApp isn't initialized.
145        if app.get_sub_app(RenderApp).is_none() {
146            return;
147        }
148        app.insert_resource(MainThreadExecutor::new());
149
150        let mut sub_app = SubApp::new();
151        sub_app.set_extract(renderer_extract);
152        app.insert_sub_app(RenderExtractApp, sub_app);
153    }
154
155    // Sets up the render thread and inserts resources into the main app used for controlling the render thread.
156    fn cleanup(&self, app: &mut App) {
157        // skip setting up when headless
158        if app.get_sub_app(RenderExtractApp).is_none() {
159            return;
160        }
161
162        let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
163        let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
164
165        let mut render_app = app
166            .remove_sub_app(RenderApp)
167            .expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin");
168
169        // clone main thread executor to render world
170        let executor = app.world().resource::<MainThreadExecutor>().clone();
171        render_app.world_mut().insert_resource(executor.clone());
172
173        render_to_app_sender.send_blocking(render_app).unwrap();
174
175        app.insert_resource(RenderAppChannels::new(
176            app_to_render_sender,
177            render_to_app_receiver,
178            executor,
179        ));
180
181        std::thread::Builder::new()
182            .name("Render thread".into())
183            .spawn(move || {
184                #[cfg(feature = "trace")]
185                let _span = bevy_log::info_span!("render thread").entered();
186
187                let compute_task_pool = ComputeTaskPool::get();
188                loop {
189                    // run a scope here to allow main world to use this thread while it's waiting for the render app
190                    let sent_app = compute_task_pool
191                        .scope(|s| {
192                            s.spawn(async { app_to_render_receiver.recv().await });
193                        })
194                        .pop();
195                    let Some(Ok(mut render_app)) = sent_app else {
196                        break;
197                    };
198
199                    {
200                        #[cfg(feature = "trace")]
201                        let _sub_app_span =
202                            bevy_log::info_span!("sub app", name = ?RenderApp).entered();
203                        render_app.update();
204                    }
205
206                    if render_to_app_sender.send_blocking(render_app).is_err() {
207                        break;
208                    }
209                }
210
211                bevy_log::debug!("exiting pipelined rendering thread");
212            })
213            .expect("Failed to create render thread");
214    }
215}
216
217// This function waits for the rendering world to be received,
218// runs extract, and then sends the rendering world back to the render thread.
219fn renderer_extract(app_world: &mut World, _world: &mut World) {
220    app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
221        world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
222            // we use a scope here to run any main thread tasks that the render world still needs to run
223            // while we wait for the render world to be received.
224            if let Some(mut render_app) = ComputeTaskPool::get()
225                .scope_with_executor(true, Some(&*main_thread_executor.0), |s| {
226                    s.spawn(async { render_channels.recv().await });
227                })
228                .pop()
229                .unwrap()
230            {
231                render_app.extract(world);
232
233                render_channels.send_blocking(render_app);
234            } else {
235                // Renderer thread panicked
236                world.write_message(AppExit::error());
237            }
238        });
239    });
240}
241
242#[cfg(all(test, feature = "multi_threaded"))]
243mod tests {
244    use super::*;
245    use bevy_tasks::{block_on, TaskPool};
246    use core::time::Duration;
247    use std::{
248        sync::mpsc,
249        thread::{self, JoinHandle, ThreadId},
250    };
251
252    const TIMEOUT: Duration = Duration::from_secs(10);
253
254    // The executor must be created and ticked on the synthetic main thread.
255    // Keep the test thread free to time out even if shutdown deadlocks.
256    fn run_on_main_thread(test: impl FnOnce() + Send + 'static) {
257        ComputeTaskPool::get_or_init(TaskPool::new);
258        let (done_sender, done_receiver) = mpsc::channel();
259        let main_thread = thread::spawn(move || {
260            test();
261            done_sender.send(()).unwrap();
262        });
263
264        done_receiver
265            .recv_timeout(TIMEOUT)
266            .expect("render shutdown did not finish");
267        main_thread.join().unwrap();
268    }
269
270    fn start_render_thread(render_app: SubApp) -> (RenderAppChannels, JoinHandle<()>) {
271        let main_thread_id = thread::current().id();
272        let executor = MainThreadExecutor::new();
273        let render_executor = executor.clone();
274        let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
275        let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
276        let mut channels =
277            RenderAppChannels::new(app_to_render_sender, render_to_app_receiver, executor);
278        channels.send_blocking(render_app);
279
280        let (queued_sender, queued_receiver) = mpsc::channel();
281        let render_thread = thread::spawn(move || {
282            let render_app = app_to_render_receiver.recv_blocking().unwrap();
283            let task = render_executor.0.spawn(async move {
284                assert_eq!(thread::current().id(), main_thread_id);
285            });
286            queued_sender.send(()).unwrap();
287            block_on(task);
288            render_to_app_sender.send_blocking(render_app).unwrap();
289        });
290
291        // Ensure shutdown encounters pending main-thread work, regardless of scheduling.
292        queued_receiver.recv_timeout(TIMEOUT).unwrap();
293        (channels, render_thread)
294    }
295
296    struct NotifyOnDrop(mpsc::Sender<ThreadId>);
297
298    impl Drop for NotifyOnDrop {
299        fn drop(&mut self) {
300            self.0.send(thread::current().id()).unwrap();
301        }
302    }
303
304    struct PanicOnDrop;
305
306    impl Drop for PanicOnDrop {
307        fn drop(&mut self) {
308            panic!("render app drop panic");
309        }
310    }
311
312    #[test]
313    fn drop_pumps_main_thread_executor_to_avoid_shutdown_deadlock() {
314        run_on_main_thread(|| {
315            let main_thread_id = thread::current().id();
316            let (dropped_sender, dropped_receiver) = mpsc::channel();
317            let mut render_app = SubApp::new();
318            render_app
319                .world_mut()
320                .insert_non_send(NotifyOnDrop(dropped_sender));
321            let (channels, render_thread) = start_render_thread(render_app);
322
323            drop(channels);
324
325            render_thread.join().unwrap();
326            assert_eq!(
327                dropped_receiver.recv_timeout(TIMEOUT).unwrap(),
328                main_thread_id
329            );
330        });
331    }
332
333    #[test]
334    fn drop_suppresses_render_app_drop_panic_during_unwind() {
335        run_on_main_thread(|| {
336            let mut render_app = SubApp::new();
337            render_app.world_mut().insert_non_send(PanicOnDrop);
338            let (channels, render_thread) = start_render_thread(render_app);
339
340            let result = catch_unwind(AssertUnwindSafe(move || {
341                let _channels = channels;
342                panic!("main app panic");
343            }));
344
345            assert_eq!(
346                result.unwrap_err().downcast_ref::<&str>(),
347                Some(&"main app panic")
348            );
349            render_thread.join().unwrap();
350        });
351    }
352
353    #[test]
354    fn drop_propagates_render_app_drop_panic_without_existing_panic() {
355        run_on_main_thread(|| {
356            let mut render_app = SubApp::new();
357            render_app.world_mut().insert_non_send(PanicOnDrop);
358            let (channels, render_thread) = start_render_thread(render_app);
359
360            let result = catch_unwind(AssertUnwindSafe(|| drop(channels)));
361
362            assert_eq!(
363                result.unwrap_err().downcast_ref::<&str>(),
364                Some(&"render app drop panic")
365            );
366            render_thread.join().unwrap();
367        });
368    }
369}