use core::panic::AssertUnwindSafe;
use std::panic::{catch_unwind, resume_unwind};
use async_channel::{Receiver, Sender};
use bevy_app::{App, AppExit, Plugin, SubApp};
use bevy_derive::AppLabel;
use bevy_ecs::{
resource::Resource,
schedule::MainThreadExecutor,
world::{Mut, World},
};
use bevy_tasks::ComputeTaskPool;
use crate::RenderApp;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel, Default)]
pub struct RenderExtractApp;
#[derive(Resource)]
pub struct RenderAppChannels {
app_to_render_sender: Sender<SubApp>,
render_to_app_receiver: Receiver<SubApp>,
render_app_in_render_thread: bool,
main_thread_executor: MainThreadExecutor,
}
impl RenderAppChannels {
pub fn new(
app_to_render_sender: Sender<SubApp>,
render_to_app_receiver: Receiver<SubApp>,
main_thread_executor: MainThreadExecutor,
) -> Self {
Self {
app_to_render_sender,
render_to_app_receiver,
render_app_in_render_thread: false,
main_thread_executor,
}
}
pub fn send_blocking(&mut self, render_app: SubApp) {
self.app_to_render_sender.send_blocking(render_app).unwrap();
self.render_app_in_render_thread = true;
}
pub async fn recv(&mut self) -> Option<SubApp> {
let render_app = self.render_to_app_receiver.recv().await.ok()?;
self.render_app_in_render_thread = false;
Some(render_app)
}
}
impl Drop for RenderAppChannels {
fn drop(&mut self) {
if self.render_app_in_render_thread {
let result = catch_unwind(AssertUnwindSafe(|| {
ComputeTaskPool::get().scope_with_executor(
true,
Some(&self.main_thread_executor.0),
|scope| {
scope.spawn(async { self.render_to_app_receiver.recv().await.ok() });
},
);
}));
if let Err(payload) = result {
if std::thread::panicking() {
core::mem::forget(payload);
} else {
resume_unwind(payload);
}
}
}
}
}
#[derive(Default)]
pub struct PipelinedRenderingPlugin;
impl Plugin for PipelinedRenderingPlugin {
fn build(&self, app: &mut App) {
if app.get_sub_app(RenderApp).is_none() {
return;
}
app.insert_resource(MainThreadExecutor::new());
let mut sub_app = SubApp::new();
sub_app.set_extract(renderer_extract);
app.insert_sub_app(RenderExtractApp, sub_app);
}
fn cleanup(&self, app: &mut App) {
if app.get_sub_app(RenderExtractApp).is_none() {
return;
}
let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
let mut render_app = app
.remove_sub_app(RenderApp)
.expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin");
let executor = app.world().resource::<MainThreadExecutor>().clone();
render_app.world_mut().insert_resource(executor.clone());
render_to_app_sender.send_blocking(render_app).unwrap();
app.insert_resource(RenderAppChannels::new(
app_to_render_sender,
render_to_app_receiver,
executor,
));
std::thread::Builder::new()
.name("Render thread".into())
.spawn(move || {
#[cfg(feature = "trace")]
let _span = bevy_log::info_span!("render thread").entered();
let compute_task_pool = ComputeTaskPool::get();
loop {
let sent_app = compute_task_pool
.scope(|s| {
s.spawn(async { app_to_render_receiver.recv().await });
})
.pop();
let Some(Ok(mut render_app)) = sent_app else {
break;
};
{
#[cfg(feature = "trace")]
let _sub_app_span =
bevy_log::info_span!("sub app", name = ?RenderApp).entered();
render_app.update();
}
if render_to_app_sender.send_blocking(render_app).is_err() {
break;
}
}
bevy_log::debug!("exiting pipelined rendering thread");
})
.expect("Failed to create render thread");
}
}
fn renderer_extract(app_world: &mut World, _world: &mut World) {
app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
if let Some(mut render_app) = ComputeTaskPool::get()
.scope_with_executor(true, Some(&*main_thread_executor.0), |s| {
s.spawn(async { render_channels.recv().await });
})
.pop()
.unwrap()
{
render_app.extract(world);
render_channels.send_blocking(render_app);
} else {
world.write_message(AppExit::error());
}
});
});
}
#[cfg(all(test, feature = "multi_threaded"))]
mod tests {
use super::*;
use bevy_tasks::{block_on, TaskPool};
use core::time::Duration;
use std::{
sync::mpsc,
thread::{self, JoinHandle, ThreadId},
};
const TIMEOUT: Duration = Duration::from_secs(10);
fn run_on_main_thread(test: impl FnOnce() + Send + 'static) {
ComputeTaskPool::get_or_init(TaskPool::new);
let (done_sender, done_receiver) = mpsc::channel();
let main_thread = thread::spawn(move || {
test();
done_sender.send(()).unwrap();
});
done_receiver
.recv_timeout(TIMEOUT)
.expect("render shutdown did not finish");
main_thread.join().unwrap();
}
fn start_render_thread(render_app: SubApp) -> (RenderAppChannels, JoinHandle<()>) {
let main_thread_id = thread::current().id();
let executor = MainThreadExecutor::new();
let render_executor = executor.clone();
let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
let mut channels =
RenderAppChannels::new(app_to_render_sender, render_to_app_receiver, executor);
channels.send_blocking(render_app);
let (queued_sender, queued_receiver) = mpsc::channel();
let render_thread = thread::spawn(move || {
let render_app = app_to_render_receiver.recv_blocking().unwrap();
let task = render_executor.0.spawn(async move {
assert_eq!(thread::current().id(), main_thread_id);
});
queued_sender.send(()).unwrap();
block_on(task);
render_to_app_sender.send_blocking(render_app).unwrap();
});
queued_receiver.recv_timeout(TIMEOUT).unwrap();
(channels, render_thread)
}
struct NotifyOnDrop(mpsc::Sender<ThreadId>);
impl Drop for NotifyOnDrop {
fn drop(&mut self) {
self.0.send(thread::current().id()).unwrap();
}
}
struct PanicOnDrop;
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!("render app drop panic");
}
}
#[test]
fn drop_pumps_main_thread_executor_to_avoid_shutdown_deadlock() {
run_on_main_thread(|| {
let main_thread_id = thread::current().id();
let (dropped_sender, dropped_receiver) = mpsc::channel();
let mut render_app = SubApp::new();
render_app
.world_mut()
.insert_non_send(NotifyOnDrop(dropped_sender));
let (channels, render_thread) = start_render_thread(render_app);
drop(channels);
render_thread.join().unwrap();
assert_eq!(
dropped_receiver.recv_timeout(TIMEOUT).unwrap(),
main_thread_id
);
});
}
#[test]
fn drop_suppresses_render_app_drop_panic_during_unwind() {
run_on_main_thread(|| {
let mut render_app = SubApp::new();
render_app.world_mut().insert_non_send(PanicOnDrop);
let (channels, render_thread) = start_render_thread(render_app);
let result = catch_unwind(AssertUnwindSafe(move || {
let _channels = channels;
panic!("main app panic");
}));
assert_eq!(
result.unwrap_err().downcast_ref::<&str>(),
Some(&"main app panic")
);
render_thread.join().unwrap();
});
}
#[test]
fn drop_propagates_render_app_drop_panic_without_existing_panic() {
run_on_main_thread(|| {
let mut render_app = SubApp::new();
render_app.world_mut().insert_non_send(PanicOnDrop);
let (channels, render_thread) = start_render_thread(render_app);
let result = catch_unwind(AssertUnwindSafe(|| drop(channels)));
assert_eq!(
result.unwrap_err().downcast_ref::<&str>(),
Some(&"render app drop panic")
);
render_thread.join().unwrap();
});
}
}