bevy_render/
pipelined_rendering.rs1use 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#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel, Default)]
22pub struct RenderExtractApp;
23
24#[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 main_thread_executor: MainThreadExecutor,
32}
33
34impl RenderAppChannels {
35 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 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 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 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 core::mem::forget(payload);
91 } else {
92 resume_unwind(payload);
93 }
94 }
95 }
96 }
97}
98
99#[derive(Default)]
140pub struct PipelinedRenderingPlugin;
141
142impl Plugin for PipelinedRenderingPlugin {
143 fn build(&self, app: &mut App) {
144 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 fn cleanup(&self, app: &mut App) {
157 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 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 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
217fn 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 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 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 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 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}