bevy_render/pipelined_rendering.rs
1use async_channel::{Receiver, Sender};
2
3use bevy_app::{App, AppExit, AppLabel, Plugin, SubApp};
4use bevy_ecs::{
5 schedule::MainThreadExecutor,
6 system::Resource,
7 world::{Mut, World},
8};
9use bevy_tasks::ComputeTaskPool;
10
11use crate::RenderApp;
12
13/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread.
14///
15/// The Main schedule of this app can be used to run logic after the render schedule starts, but
16/// before I/O processing. This can be useful for something like frame pacing.
17#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
18pub struct RenderExtractApp;
19
20/// Channels used by the main app to send and receive the render app.
21#[derive(Resource)]
22pub struct RenderAppChannels {
23 app_to_render_sender: Sender<SubApp>,
24 render_to_app_receiver: Receiver<SubApp>,
25 render_app_in_render_thread: bool,
26}
27
28impl RenderAppChannels {
29 /// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`]
30 pub fn new(
31 app_to_render_sender: Sender<SubApp>,
32 render_to_app_receiver: Receiver<SubApp>,
33 ) -> Self {
34 Self {
35 app_to_render_sender,
36 render_to_app_receiver,
37 render_app_in_render_thread: false,
38 }
39 }
40
41 /// Send the `render_app` to the rendering thread.
42 pub fn send_blocking(&mut self, render_app: SubApp) {
43 self.app_to_render_sender.send_blocking(render_app).unwrap();
44 self.render_app_in_render_thread = true;
45 }
46
47 /// Receive the `render_app` from the rendering thread.
48 /// Return `None` if the render thread has panicked.
49 pub async fn recv(&mut self) -> Option<SubApp> {
50 let render_app = self.render_to_app_receiver.recv().await.ok()?;
51 self.render_app_in_render_thread = false;
52 Some(render_app)
53 }
54}
55
56impl Drop for RenderAppChannels {
57 fn drop(&mut self) {
58 if self.render_app_in_render_thread {
59 // Any non-send data in the render world was initialized on the main thread.
60 // So on dropping the main world and ending the app, we block and wait for
61 // the render world to return to drop it. Which allows the non-send data
62 // drop methods to run on the correct thread.
63 self.render_to_app_receiver.recv_blocking().ok();
64 }
65 }
66}
67
68/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering.
69/// This moves rendering into a different thread, so that the Nth frame's rendering can
70/// be run at the same time as the N + 1 frame's simulation.
71///
72/// ```text
73/// |--------------------|--------------------|--------------------|--------------------|
74/// | simulation thread | frame 1 simulation | frame 2 simulation | frame 3 simulation |
75/// |--------------------|--------------------|--------------------|--------------------|
76/// | rendering thread | | frame 1 rendering | frame 2 rendering |
77/// |--------------------|--------------------|--------------------|--------------------|
78/// ```
79///
80/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must
81/// be added after that plugin. If it is not added after, the plugin will do nothing.
82///
83/// A single frame of execution looks something like below
84///
85/// ```text
86/// |--------------------------------------------------------------------|
87/// | | RenderExtractApp schedule | winit events | main schedule |
88/// | extract |----------------------------------------------------------|
89/// | | extract commands | rendering schedule |
90/// |--------------------------------------------------------------------|
91/// ```
92///
93/// - `extract` is the step where data is copied from the main world to the render world.
94/// This is run on the main app's thread.
95/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the
96/// main schedule can start sooner.
97/// - Then the `rendering schedule` is run. See [`RenderSet`](crate::RenderSet) for the standard steps in this process.
98/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By
99/// default this schedule is empty. But it is useful if you need something to run before I/O processing.
100/// - Next all the `winit events` are processed.
101/// - And finally the `main app schedule` is run.
102/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.
103#[derive(Default)]
104pub struct PipelinedRenderingPlugin;
105
106impl Plugin for PipelinedRenderingPlugin {
107 fn build(&self, app: &mut App) {
108 // Don't add RenderExtractApp if RenderApp isn't initialized.
109 if app.get_sub_app(RenderApp).is_none() {
110 return;
111 }
112 app.insert_resource(MainThreadExecutor::new());
113
114 let mut sub_app = SubApp::new();
115 sub_app.set_extract(renderer_extract);
116 app.insert_sub_app(RenderExtractApp, sub_app);
117 }
118
119 // Sets up the render thread and inserts resources into the main app used for controlling the render thread.
120 fn cleanup(&self, app: &mut App) {
121 // skip setting up when headless
122 if app.get_sub_app(RenderExtractApp).is_none() {
123 return;
124 }
125
126 let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
127 let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
128
129 let mut render_app = app
130 .remove_sub_app(RenderApp)
131 .expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin");
132
133 // clone main thread executor to render world
134 let executor = app.world().get_resource::<MainThreadExecutor>().unwrap();
135 render_app.world_mut().insert_resource(executor.clone());
136
137 render_to_app_sender.send_blocking(render_app).unwrap();
138
139 app.insert_resource(RenderAppChannels::new(
140 app_to_render_sender,
141 render_to_app_receiver,
142 ));
143
144 std::thread::spawn(move || {
145 #[cfg(feature = "trace")]
146 let _span = bevy_utils::tracing::info_span!("render thread").entered();
147
148 let compute_task_pool = ComputeTaskPool::get();
149 loop {
150 // run a scope here to allow main world to use this thread while it's waiting for the render app
151 let sent_app = compute_task_pool
152 .scope(|s| {
153 s.spawn(async { app_to_render_receiver.recv().await });
154 })
155 .pop();
156 let Some(Ok(mut render_app)) = sent_app else {
157 break;
158 };
159
160 {
161 #[cfg(feature = "trace")]
162 let _sub_app_span =
163 bevy_utils::tracing::info_span!("sub app", name = ?RenderApp).entered();
164 render_app.update();
165 }
166
167 if render_to_app_sender.send_blocking(render_app).is_err() {
168 break;
169 }
170 }
171
172 bevy_utils::tracing::debug!("exiting pipelined rendering thread");
173 });
174 }
175}
176
177// This function waits for the rendering world to be received,
178// runs extract, and then sends the rendering world back to the render thread.
179fn renderer_extract(app_world: &mut World, _world: &mut World) {
180 app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
181 world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
182 // we use a scope here to run any main thread tasks that the render world still needs to run
183 // while we wait for the render world to be received.
184 if let Some(mut render_app) = ComputeTaskPool::get()
185 .scope_with_executor(true, Some(&*main_thread_executor.0), |s| {
186 s.spawn(async { render_channels.recv().await });
187 })
188 .pop()
189 .unwrap()
190 {
191 render_app.extract(world);
192
193 render_channels.send_blocking(render_app);
194 } else {
195 // Renderer thread panicked
196 world.send_event(AppExit::error());
197 }
198 });
199 });
200}